diff --git a/.gitignore b/.gitignore index 073a29d111..4e2183af5c 100644 --- a/.gitignore +++ b/.gitignore @@ -144,3 +144,5 @@ build/docs.zip build/ui-docs.zip build/csharp-docs.zip build/msbuild.log + +src/packages/ \ No newline at end of file diff --git a/README.md b/README.md index 53070b6917..bce027090f 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,13 @@ Umbraco CMS Umbraco is a free open source Content Management System built on the ASP.NET platform. ## 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 dev` in `src\Umbraco.Web.UI.Client`. +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 make sure to read the [Belle ReadMe file](src/Umbraco.Web.UI.Client/README.md). Note that you can always [download a nightly build](http://nightly.umbraco.org/umbraco%207.0.0/) so you don't have to build the code yourself. +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). -## Watch a introduction video ## +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. + +## Watch an introduction video ## [![ScreenShot](http://umbraco.com/images/whatisumbraco.png)](https://umbraco.tv/videos/umbraco-v7/content-editor/basics/introduction/cms-explanation/) diff --git a/appveyor.yml b/appveyor.yml index 559793130d..d5c79b314d 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -36,7 +36,9 @@ test: assemblies: src\Umbraco.Tests\bin\Debug\Umbraco.Tests.dll artifacts: - path: build\UmbracoCms.* + name: UmbracoFiles - path: build\msbuild.log + name: BuildLog notifications: - provider: Slack auth_token: diff --git a/build/InstallGit.cmd b/build/InstallGit.cmd index ca66aed877..4daa2f45d9 100644 --- a/build/InstallGit.cmd +++ b/build/InstallGit.cmd @@ -18,6 +18,14 @@ GOTO :EOF ECHO Git is not in your path and could not be found in C:\Program Files (x86)\Git\cmd nor in C:\Program Files\Git\cmd SET /p install=" Do you want to install Git through Chocolatey [y/n]? " %=% IF %install%==y ( + :: Create a temporary batch file to execute either after elevating to admin or as-is when the user is already admin + ECHO @ECHO OFF > "%temp%\ChocoInstallGit.cmd" + ECHO SETLOCAL >> "%temp%\ChocoInstallGit.cmd" + ECHO ECHO Installing Chocolatey first >> "%temp%\ChocoInstallGit.cmd" + ECHO @powershell -NoProfile -ExecutionPolicy Bypass -Command "iex ((new-object net.webclient).DownloadString('https://chocolatey.org/install.ps1'))" >> "%temp%\ChocoInstallGit.cmd" + ECHO SET PATH=%%PATH%%;%%ALLUSERSPROFILE%%\chocolatey\bin >> "%temp%\ChocoInstallGit.cmd" + ECHO choco install git -y >> "%temp%\ChocoInstallGit.cmd" + GOTO :installgit ) ELSE ( GOTO :cantcontinue @@ -28,7 +36,28 @@ ECHO Can't complete the build without Git being in the path. Please add it to be GOTO :EOF :installgit -ECHO Installing Chocolatey first -@powershell -NoProfile -ExecutionPolicy unrestricted -Command "iex ((new-object net.webclient).DownloadString('https://chocolatey.org/install.ps1'))" && SET PATH=%PATH%;%ALLUSERSPROFILE%\chocolatey\bin -ECHO Installing Git through Chocolatey -choco install git \ No newline at end of file +pushd %~dp0 + :: Running prompt elevated + +:: --> Check for permissions +>nul 2>&1 "%SYSTEMROOT%\system32\cacls.exe" "%SYSTEMROOT%\system32\config\system" + +:: --> If error flag set, we do not have admin. +IF '%errorlevel%' NEQ '0' ( + GOTO UACPrompt +) ELSE ( GOTO gotAdmin ) + +:UACPrompt + ECHO You're not currently running this with admin privileges, we'll now try to execute the install of Git through Chocolatey after elevating to admin privileges + ECHO Set UAC = CreateObject^("Shell.Application"^) > "%temp%\getadmin.vbs" + ECHO UAC.ShellExecute "%temp%\ChocoInstallGit.cmd", "", "", "runas", 1 >> "%temp%\getadmin.vbs" + + "%temp%\getadmin.vbs" + EXIT /B + +:gotAdmin + IF EXIST "%temp%\getadmin.vbs" ( DEL "%temp%\getadmin.vbs" ) + pushd "%CD%" + CD /D "%~dp0" + + CALL "%temp%\ChocoInstallGit.cmd" \ No newline at end of file diff --git a/build/NuSpecs/UmbracoCms.Core.nuspec b/build/NuSpecs/UmbracoCms.Core.nuspec index d5315acf45..145e3a96ba 100644 --- a/build/NuSpecs/UmbracoCms.Core.nuspec +++ b/build/NuSpecs/UmbracoCms.Core.nuspec @@ -1,88 +1,90 @@ - - - - UmbracoCms.Core - 7.0.0 - Umbraco Cms Core Binaries - Umbraco HQ - Umbraco HQ - http://opensource.org/licenses/MIT - http://umbraco.com/ - http://umbraco.com/media/357769/100px_transparent.png - false - Contains the core assemblies needed to run Umbraco Cms. This package only contains assemblies and can be used for package development. Use the UmbracoCms-package to setup Umbraco in Visual Studio as an ASP.NET project. - Contains the core assemblies needed to run Umbraco Cms - en-US - umbraco - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + UmbracoCms.Core + 7.0.0 + Umbraco Cms Core Binaries + Umbraco HQ + Umbraco HQ + http://opensource.org/licenses/MIT + http://umbraco.com/ + http://umbraco.com/media/357769/100px_transparent.png + false + Contains the core assemblies needed to run Umbraco Cms. This package only contains assemblies and can be used for package development. Use the UmbracoCms-package to setup Umbraco in Visual Studio as an ASP.NET project. + Contains the core assemblies needed to run Umbraco Cms + en-US + umbraco + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/build/NuSpecs/UmbracoCms.nuspec b/build/NuSpecs/UmbracoCms.nuspec index 5d1329da46..22bbf5a58d 100644 --- a/build/NuSpecs/UmbracoCms.nuspec +++ b/build/NuSpecs/UmbracoCms.nuspec @@ -1,50 +1,50 @@ - - - - UmbracoCms - 7.0.0 - Umbraco Cms - Umbraco HQ - Umbraco HQ - http://opensource.org/licenses/MIT - http://umbraco.com/ - http://umbraco.com/media/357769/100px_transparent.png - false - Installs Umbraco Cms in your Visual Studio ASP.NET project - Installs Umbraco Cms in your Visual Studio ASP.NET project - en-US - umbraco - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + UmbracoCms + 7.0.0 + Umbraco Cms + Umbraco HQ + Umbraco HQ + http://opensource.org/licenses/MIT + http://umbraco.com/ + http://umbraco.com/media/357769/100px_transparent.png + false + Installs Umbraco Cms in your Visual Studio ASP.NET project + Installs Umbraco Cms in your Visual Studio ASP.NET project + en-US + umbraco + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/build/NuSpecs/tools/Dashboard.config.install.xdt b/build/NuSpecs/tools/Dashboard.config.install.xdt index a77632926c..8368870186 100644 --- a/build/NuSpecs/tools/Dashboard.config.install.xdt +++ b/build/NuSpecs/tools/Dashboard.config.install.xdt @@ -21,6 +21,12 @@
+ + developer + +
+ +
views/dashboard/developer/developerdashboardvideos.html @@ -30,16 +36,21 @@
- + views/dashboard/developer/examinemanagement.html - - + + - views/dashboard/developer/xmldataintegrityreport.html + views/dashboard/developer/healthcheck.html - + + + + views/dashboard/developer/redirecturls.html + +
@@ -52,19 +63,6 @@
-
- - - - admin - - - views/dashboard/default/startupdashboardintro.html - - - -
-
@@ -80,5 +78,6 @@
+
\ No newline at end of file diff --git a/build/NuSpecs/tools/Web.config.install.xdt b/build/NuSpecs/tools/Web.config.install.xdt index dfb9925840..2d5dfb0bdb 100644 --- a/build/NuSpecs/tools/Web.config.install.xdt +++ b/build/NuSpecs/tools/Web.config.install.xdt @@ -292,6 +292,8 @@ + + @@ -317,6 +319,7 @@ + @@ -336,6 +339,10 @@ + + + + diff --git a/build/NuSpecs/tools/trees.config.install.xdt b/build/NuSpecs/tools/trees.config.install.xdt index 1a56c81271..e6c9e074ab 100644 --- a/build/NuSpecs/tools/trees.config.install.xdt +++ b/build/NuSpecs/tools/trees.config.install.xdt @@ -55,21 +55,29 @@ xdt:Transform="SetAttributes()" /> - - + xdt:Transform="Remove" /> + + + + + + + + - - diff --git a/src/SQLCE4Umbraco/SqlCEHelper.cs b/src/SQLCE4Umbraco/SqlCEHelper.cs index ab6f686c21..a8567abd67 100644 --- a/src/SQLCE4Umbraco/SqlCEHelper.cs +++ b/src/SQLCE4Umbraco/SqlCEHelper.cs @@ -180,12 +180,16 @@ namespace SqlCE4Umbraco /// The return value of the command. protected override object ExecuteScalar(string commandText, SqlCeParameter[] parameters) { - #if DEBUG && DebugDataLayer +#if DEBUG && DebugDataLayer // Log Query Execution Trace.TraceInformation(GetType().Name + " SQL ExecuteScalar: " + commandText); - #endif - - return SqlCeApplicationBlock.ExecuteScalar(ConnectionString, CommandType.Text, commandText, parameters); +#endif + using (var cc = UseCurrentConnection) + { + return SqlCeApplicationBlock.ExecuteScalar( + (SqlCeConnection) cc.Connection, (SqlCeTransaction) cc.Transaction, + CommandType.Text, commandText, parameters); + } } /// @@ -198,12 +202,17 @@ namespace SqlCE4Umbraco /// protected override int ExecuteNonQuery(string commandText, SqlCeParameter[] parameters) { - #if DEBUG && DebugDataLayer +#if DEBUG && DebugDataLayer // Log Query Execution Trace.TraceInformation(GetType().Name + " SQL ExecuteNonQuery: " + commandText); - #endif +#endif - return SqlCeApplicationBlock.ExecuteNonQuery(ConnectionString, CommandType.Text, commandText, parameters); + using (var cc = UseCurrentConnection) + { + return SqlCeApplicationBlock.ExecuteNonQuery( + (SqlCeConnection) cc.Connection, (SqlCeTransaction) cc.Transaction, + CommandType.Text, commandText, parameters); + } } /// @@ -216,13 +225,17 @@ namespace SqlCE4Umbraco /// protected override IRecordsReader ExecuteReader(string commandText, SqlCeParameter[] parameters) { - #if DEBUG && DebugDataLayer +#if DEBUG && DebugDataLayer // Log Query Execution Trace.TraceInformation(GetType().Name + " SQL ExecuteReader: " + commandText); - #endif +#endif - return new SqlCeDataReaderHelper(SqlCeApplicationBlock.ExecuteReader(ConnectionString, CommandType.Text, - commandText, parameters)); + using (var cc = UseCurrentConnection) + { + return new SqlCeDataReaderHelper(SqlCeApplicationBlock.ExecuteReader( + (SqlCeConnection) cc.Connection, (SqlCeTransaction) cc.Transaction, + CommandType.Text, commandText, parameters)); + } } diff --git a/src/SQLCE4Umbraco/SqlCeApplicationBlock.cs b/src/SQLCE4Umbraco/SqlCeApplicationBlock.cs index 5cd9bc2140..fd778bbfb3 100644 --- a/src/SQLCE4Umbraco/SqlCeApplicationBlock.cs +++ b/src/SQLCE4Umbraco/SqlCeApplicationBlock.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Data.SqlServerCe; using System.Data; using System.Diagnostics; @@ -26,30 +24,61 @@ namespace SqlCE4Umbraco params SqlCeParameter[] commandParameters ) { - object retVal; - try { - using (SqlCeConnection conn = SqlCeContextGuardian.Open(connectionString)) + using (var conn = SqlCeContextGuardian.Open(connectionString)) { - using (SqlCeCommand cmd = new SqlCeCommand(commandText, conn)) - { - AttachParameters(cmd, commandParameters); - Debug.WriteLine("---------------------------------SCALAR-------------------------------------"); - Debug.WriteLine(commandText); - Debug.WriteLine("----------------------------------------------------------------------------"); - retVal = cmd.ExecuteScalar(); - } + return ExecuteScalarTry(conn, null, commandText, commandParameters); } - - return retVal; } catch (Exception ee) { - throw new SqlCeProviderException("Error running Scalar: \nSQL Statement:\n" + commandText + "\n\nException:\n" + ee.ToString()); + throw new SqlCeProviderException("Error running Scalar: \nSQL Statement:\n" + commandText + "\n\nException:\n" + ee); } } + public static object ExecuteScalar( + SqlCeConnection conn, SqlCeTransaction trx, + CommandType commandType, + string commandText, + params SqlCeParameter[] commandParameters) + { + try + { + return ExecuteScalarTry(conn, trx, commandText, commandParameters); + } + catch (Exception ee) + { + throw new SqlCeProviderException("Error running Scalar: \nSQL Statement:\n" + commandText + "\n\nException:\n" + ee); + } + } + + public static object ExecuteScalar( + SqlCeConnection conn, + CommandType commandType, + string commandText, + params SqlCeParameter[] commandParameters) + { + return ExecuteScalar(conn, null, commandType, commandText, commandParameters); + } + + private static object ExecuteScalarTry( + SqlCeConnection conn, SqlCeTransaction trx, + string commandText, + params SqlCeParameter[] commandParameters) + { + object retVal; + using (var cmd = trx == null ? new SqlCeCommand(commandText, conn) : new SqlCeCommand(commandText, conn, trx)) + { + AttachParameters(cmd, commandParameters); + Debug.WriteLine("---------------------------------SCALAR-------------------------------------"); + Debug.WriteLine(commandText); + Debug.WriteLine("----------------------------------------------------------------------------"); + retVal = cmd.ExecuteScalar(); + } + return retVal; + } + /// /// /// @@ -66,49 +95,10 @@ namespace SqlCE4Umbraco { try { - int rowsAffected; - using (SqlCeConnection conn = SqlCeContextGuardian.Open(connectionString)) + using (var conn = SqlCeContextGuardian.Open(connectionString)) { - // this is for multiple queries in the installer - if (commandText.Trim().StartsWith("!!!")) - { - commandText = commandText.Trim().Trim('!'); - string[] commands = commandText.Split('|'); - string currentCmd = String.Empty; - - foreach (string cmd in commands) - { - try - { - currentCmd = cmd; - if (!String.IsNullOrWhiteSpace(cmd)) - { - SqlCeCommand c = new SqlCeCommand(cmd, conn); - c.ExecuteNonQuery(); - } - } - catch (Exception e) - { - Debug.WriteLine("*******************************************************************"); - Debug.WriteLine(currentCmd); - Debug.WriteLine(e); - Debug.WriteLine("*******************************************************************"); - } - } - return 1; - } - else - { - Debug.WriteLine("----------------------------------------------------------------------------"); - Debug.WriteLine(commandText); - Debug.WriteLine("----------------------------------------------------------------------------"); - SqlCeCommand cmd = new SqlCeCommand(commandText, conn); - AttachParameters(cmd, commandParameters); - rowsAffected = cmd.ExecuteNonQuery(); - } + return ExecuteNonQueryTry(conn, null, commandText, commandParameters); } - - return rowsAffected; } catch (Exception ee) { @@ -116,6 +106,74 @@ namespace SqlCE4Umbraco } } + public static int ExecuteNonQuery( + SqlCeConnection conn, + CommandType commandType, + string commandText, + params SqlCeParameter[] commandParameters + ) + { + return ExecuteNonQuery(conn, null, commandType, commandText, commandParameters); + } + + public static int ExecuteNonQuery( + SqlCeConnection conn, SqlCeTransaction trx, + CommandType commandType, + string commandText, + params SqlCeParameter[] commandParameters + ) + { + try + { + return ExecuteNonQueryTry(conn, trx, commandText, commandParameters); + } + catch (Exception ee) + { + throw new SqlCeProviderException("Error running NonQuery: \nSQL Statement:\n" + commandText + "\n\nException:\n" + ee.ToString()); + } + } + + private static int ExecuteNonQueryTry( + SqlCeConnection conn, SqlCeTransaction trx, + string commandText, + params SqlCeParameter[] commandParameters) + { + // this is for multiple queries in the installer + if (commandText.Trim().StartsWith("!!!")) + { + commandText = commandText.Trim().Trim('!'); + var commands = commandText.Split('|'); + var currentCmd = string.Empty; + + foreach (var command in commands) + { + try + { + currentCmd = command; + if (string.IsNullOrWhiteSpace(command)) continue; + var c = trx == null ? new SqlCeCommand(command, conn) : new SqlCeCommand(command, conn, trx); + c.ExecuteNonQuery(); + } + catch (Exception e) + { + Debug.WriteLine("*******************************************************************"); + Debug.WriteLine(currentCmd); + Debug.WriteLine(e); + Debug.WriteLine("*******************************************************************"); + } + } + return 1; + } + + Debug.WriteLine("----------------------------------------------------------------------------"); + Debug.WriteLine(commandText); + Debug.WriteLine("----------------------------------------------------------------------------"); + var cmd = new SqlCeCommand(commandText, conn); + AttachParameters(cmd, commandParameters); + var rowsAffected = cmd.ExecuteNonQuery(); + return rowsAffected; + } + /// /// /// @@ -133,25 +191,8 @@ namespace SqlCE4Umbraco { try { - Debug.WriteLine("---------------------------------READER-------------------------------------"); - Debug.WriteLine(commandText); - Debug.WriteLine("----------------------------------------------------------------------------"); - SqlCeDataReader reader; - SqlCeConnection conn = SqlCeContextGuardian.Open(connectionString); - - try - { - SqlCeCommand cmd = new SqlCeCommand(commandText, conn); - AttachParameters(cmd, commandParameters); - reader = cmd.ExecuteReader(CommandBehavior.CloseConnection); - } - catch - { - conn.Close(); - throw; - } - - return reader; + var conn = SqlCeContextGuardian.Open(connectionString); + return ExecuteReaderTry(conn, null, commandText, commandParameters); } catch (Exception ee) { @@ -159,30 +200,71 @@ namespace SqlCE4Umbraco } } - public static bool VerifyConnection(string connectionString) + public static SqlCeDataReader ExecuteReader( + SqlCeConnection conn, + CommandType commandType, + string commandText, + params SqlCeParameter[] commandParameters + ) { - bool isConnected = false; - using (SqlCeConnection conn = SqlCeContextGuardian.Open(connectionString)) - { - isConnected = conn.State == ConnectionState.Open; - } - - return isConnected; + return ExecuteReader(conn, commandType, commandText, commandParameters); } - private static void AttachParameters(SqlCeCommand command, SqlCeParameter[] commandParameters) + public static SqlCeDataReader ExecuteReader( + SqlCeConnection conn, SqlCeTransaction trx, + CommandType commandType, + string commandText, + params SqlCeParameter[] commandParameters + ) { - foreach (SqlCeParameter parameter in commandParameters) + try + { + return ExecuteReaderTry(conn, trx, commandText, commandParameters); + } + catch (Exception ee) + { + throw new SqlCeProviderException("Error running Reader: \nSQL Statement:\n" + commandText + "\n\nException:\n" + ee.ToString()); + } + } + + private static SqlCeDataReader ExecuteReaderTry( + SqlCeConnection conn, SqlCeTransaction trx, + string commandText, + params SqlCeParameter[] commandParameters) + { + Debug.WriteLine("---------------------------------READER-------------------------------------"); + Debug.WriteLine(commandText); + Debug.WriteLine("----------------------------------------------------------------------------"); + + try + { + var cmd = trx == null ? new SqlCeCommand(commandText, conn) : new SqlCeCommand(commandText, conn, trx); + AttachParameters(cmd, commandParameters); + return cmd.ExecuteReader(CommandBehavior.CloseConnection); + } + catch + { + conn.Close(); + throw; + } + } + + public static bool VerifyConnection(string connectionString) + { + using (var conn = SqlCeContextGuardian.Open(connectionString)) + { + return conn.State == ConnectionState.Open; + } + } + + private static void AttachParameters(SqlCeCommand command, IEnumerable commandParameters) + { + foreach (var parameter in commandParameters) { if ((parameter.Direction == ParameterDirection.InputOutput) && (parameter.Value == null)) - { parameter.Value = DBNull.Value; - } command.Parameters.Add(parameter); } } - - - } } diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index cdbbd207ce..c9d7a5f7b7 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -12,4 +12,4 @@ using System.Resources; [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyFileVersion("8.0.0")] -[assembly: AssemblyInformationalVersion("8.0.0-alpha0020")] \ No newline at end of file +[assembly: AssemblyInformationalVersion("8.0.0-alpha0021")] \ No newline at end of file diff --git a/src/Umbraco.Core/ByteArrayExtensions.cs b/src/Umbraco.Core/ByteArrayExtensions.cs new file mode 100644 index 0000000000..dacdd509ca --- /dev/null +++ b/src/Umbraco.Core/ByteArrayExtensions.cs @@ -0,0 +1,39 @@ +namespace Umbraco.Core +{ + public static class ByteArrayExtensions + { + private static readonly char[] BytesToHexStringLookup = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; + + public static string ToHexString(this byte[] bytes) + { + int i = 0, p = 0, bytesLength = bytes.Length; + var chars = new char[bytesLength * 2]; + while (i < bytesLength) + { + var b = bytes[i++]; + chars[p++] = BytesToHexStringLookup[b / 0x10]; + chars[p++] = BytesToHexStringLookup[b % 0x10]; + } + return new string(chars, 0, chars.Length); + } + + public static string ToHexString(this byte[] bytes, char separator, int blockSize, int blockCount) + { + int p = 0, bytesLength = bytes.Length, count = 0, size = 0; + var chars = new char[bytesLength * 2 + blockCount]; + for (var i = 0; i < bytesLength; i++) + { + var b = bytes[i++]; + chars[p++] = BytesToHexStringLookup[b / 0x10]; + chars[p++] = BytesToHexStringLookup[b % 0x10]; + if (count == blockCount) continue; + if (++size < blockSize) continue; + + chars[p++] = '/'; + size = 0; + count++; + } + return new string(chars, 0, chars.Length); + } + } +} diff --git a/src/Umbraco.Core/Cache/HttpRequestCacheProvider.cs b/src/Umbraco.Core/Cache/HttpRequestCacheProvider.cs index ca1f4e85a0..792b5982b1 100644 --- a/src/Umbraco.Core/Cache/HttpRequestCacheProvider.cs +++ b/src/Umbraco.Core/Cache/HttpRequestCacheProvider.cs @@ -9,6 +9,9 @@ namespace Umbraco.Core.Cache /// /// A cache provider that caches items in the HttpContext.Items /// + /// + /// If the Items collection is null, then this provider has no effect + /// internal class HttpRequestCacheProvider : DictionaryCacheProviderBase { // context provider @@ -34,6 +37,11 @@ namespace Umbraco.Core.Cache get { return _context != null ? _context.Items : HttpContext.Current.Items; } } + private bool HasContextItems + { + get { return (_context != null && _context.Items != null) || HttpContext.Current != null; } + } + // for unit tests public HttpRequestCacheProvider(HttpContextBase context) { @@ -50,18 +58,23 @@ namespace Umbraco.Core.Cache protected override IEnumerable GetDictionaryEntries() { const string prefix = CacheItemPrefix + "-"; + + if (HasContextItems == false) return Enumerable.Empty(); + return ContextItems.Cast() .Where(x => x.Key is string && ((string)x.Key).StartsWith(prefix)); } protected override void RemoveEntry(string key) { + if (HasContextItems == false) return; + ContextItems.Remove(key); } protected override object GetEntry(string key) { - return ContextItems[key]; + return HasContextItems ? ContextItems[key] : null; } #region Lock @@ -81,7 +94,9 @@ namespace Umbraco.Core.Cache get { - return new MonitorLock(ContextItems.SyncRoot); + return HasContextItems + ? (IDisposable) new MonitorLock(ContextItems.SyncRoot) + : new NoopLocker(); } } @@ -91,6 +106,9 @@ namespace Umbraco.Core.Cache public override object GetCacheItem(string cacheKey, Func getCacheItem) { + //no place to cache so just return the callback result + if (HasContextItems == false) return getCacheItem(); + cacheKey = GetCacheKey(cacheKey); Lazy result; @@ -128,5 +146,10 @@ namespace Umbraco.Core.Cache #region Insert #endregion + private class NoopLocker : DisposableObject + { + protected override void DisposeResources() + { } + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/HttpRuntimeCacheProvider.cs b/src/Umbraco.Core/Cache/HttpRuntimeCacheProvider.cs index e7f5d17b83..0e98cd5318 100644 --- a/src/Umbraco.Core/Cache/HttpRuntimeCacheProvider.cs +++ b/src/Umbraco.Core/Cache/HttpRuntimeCacheProvider.cs @@ -154,7 +154,7 @@ namespace Umbraco.Core.Cache value = result.Value; // will not throw (safe lazy) var eh = value as ExceptionHolder; - if (eh != null) throw eh.Exception; // throw once! + if (eh != null) throw new Exception("Exception while creating a value.", eh.Exception); // throw once! return value; } diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs index 715c0be157..12df34d05d 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs @@ -244,7 +244,19 @@ namespace Umbraco.Core.Configuration.UmbracoSettings true); } } - + + [ConfigurationProperty("EnableInheritedMediaTypes")] + internal InnerTextConfigurationElement EnableInheritedMediaTypes + { + get + { + return new OptionalInnerTextConfigurationElement( + (InnerTextConfigurationElement)this["EnableInheritedMediaTypes"], + //set the default + true); + } + } + string IContentSection.NotificationEmailAddress { get { return Notifications.NotificationEmailAddress; } @@ -369,5 +381,10 @@ namespace Umbraco.Core.Configuration.UmbracoSettings { get { return EnableInheritedDocumentTypes; } } + + bool IContentSection.EnableInheritedMediaTypes + { + get { return EnableInheritedMediaTypes; } + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs index 34e19379aa..b80b3a8111 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs @@ -56,5 +56,7 @@ namespace Umbraco.Core.Configuration.UmbracoSettings string DefaultDocumentTypeProperty { get; } bool EnableInheritedDocumentTypes { get; } + + bool EnableInheritedMediaTypes { get; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/IWebRoutingSection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/IWebRoutingSection.cs index 2998fc2f78..9eb6d02aa7 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/IWebRoutingSection.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/IWebRoutingSection.cs @@ -9,6 +9,8 @@ bool DisableAlternativeTemplates { get; } bool DisableFindContentByIdPath { get; } + + bool DisableRedirectUrlTracking { get; } string UrlProviderMode { get; } diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/WebRoutingElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/WebRoutingElement.cs index 1ed9bc034c..82f5d46b28 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/WebRoutingElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/WebRoutingElement.cs @@ -27,6 +27,12 @@ namespace Umbraco.Core.Configuration.UmbracoSettings get { return (bool) base["disableFindContentByIdPath"]; } } + [ConfigurationProperty("disableRedirectUrlTracking", DefaultValue = "false")] + public bool DisableRedirectUrlTracking + { + get { return (bool) base["disableRedirectUrlTracking"]; } + } + [ConfigurationProperty("urlProviderMode", DefaultValue = "AutoLegacy")] public string UrlProviderMode { diff --git a/src/Umbraco.Core/Configuration/UmbracoVersion.cs b/src/Umbraco.Core/Configuration/UmbracoVersion.cs index f0571572fb..1953b1c2b2 100644 --- a/src/Umbraco.Core/Configuration/UmbracoVersion.cs +++ b/src/Umbraco.Core/Configuration/UmbracoVersion.cs @@ -21,7 +21,7 @@ namespace Umbraco.Core.Configuration /// /// Gets the version comment of the executing code (eg "beta"). /// - public static string CurrentComment => "alpha0020"; + public static string CurrentComment => "alpha0021"; /// /// Gets the assembly version of Umbraco.Code.dll. diff --git a/src/Umbraco.Core/Constants-Examine.cs b/src/Umbraco.Core/Constants-Examine.cs index 874965be6e..d982214341 100644 --- a/src/Umbraco.Core/Constants-Examine.cs +++ b/src/Umbraco.Core/Constants-Examine.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Umbraco.Core +namespace Umbraco.Core { public static partial class Constants { @@ -19,6 +13,11 @@ namespace Umbraco.Core /// The alias of the internal content indexer /// public const string InternalIndexer = "InternalIndexer"; + + /// + /// The alias of the external content indexer + /// + public const string ExternalIndexer = "ExternalIndexer"; } } } diff --git a/src/Umbraco.Core/Constants-System.cs b/src/Umbraco.Core/Constants-System.cs index b173cb17f3..3c1a34fee7 100644 --- a/src/Umbraco.Core/Constants-System.cs +++ b/src/Umbraco.Core/Constants-System.cs @@ -1,26 +1,33 @@ namespace Umbraco.Core { - public static partial class Constants - { - /// - /// Defines the identifiers for Umbraco system nodes. - /// - public static class System - { - /// - /// The integer identifier for global system root node. - /// - public const int Root = -1; + public static partial class Constants + { + /// + /// Defines the identifiers for Umbraco system nodes. + /// + public static class System + { + /// + /// The integer identifier for global system root node. + /// + public const int Root = -1; - /// - /// The integer identifier for content's recycle bin. - /// - public const int RecycleBinContent = -20; + /// + /// The integer identifier for content's recycle bin. + /// + public const int RecycleBinContent = -20; - /// - /// The integer identifier for media's recycle bin. - /// - public const int RecycleBinMedia = -21; - } + /// + /// The integer identifier for media's recycle bin. + /// + public const int RecycleBinMedia = -21; + } + + public static class DatabaseProviders + { + public const string SqlCe = "System.Data.SqlServerCe.4.0"; + public const string SqlServer = "System.Data.SqlClient"; + public const string MySql = "MySql.Data.MySqlClient"; + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/CoreRuntime.cs b/src/Umbraco.Core/CoreRuntime.cs index cab92ac7a1..e645f96599 100644 --- a/src/Umbraco.Core/CoreRuntime.cs +++ b/src/Umbraco.Core/CoreRuntime.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Configuration; using System.Threading; +using System.Web; using LightInject; using Semver; using Umbraco.Core.Cache; @@ -78,6 +79,9 @@ namespace Umbraco.Core "Booted.", "Boot failed.")) { + // throws if not full-trust + new AspNetHostingPermission(AspNetHostingPermissionLevel.Unrestricted).Demand(); + try { Logger.Debug($"Runtime: {GetType().FullName}"); diff --git a/src/Umbraco.Core/CoreRuntimeComponent.cs b/src/Umbraco.Core/CoreRuntimeComponent.cs index 63f442276e..476c3a43d6 100644 --- a/src/Umbraco.Core/CoreRuntimeComponent.cs +++ b/src/Umbraco.Core/CoreRuntimeComponent.cs @@ -39,8 +39,15 @@ namespace Umbraco.Core //TODO: Don't think we'll need this when the resolvers are all container resolvers composition.Container.RegisterSingleton(); - composition.Container.Register(factory - => FileSystemProviderManager.Current.GetFileSystemProvider()); + // register filesystems + composition.Container.Register(); + composition.Container.Register(factory => factory.GetInstance().MediaFileSystem); + composition.Container.RegisterSingleton(factory => factory.GetInstance().ScriptsFileSystem, "ScriptFileSystem"); + composition.Container.RegisterSingleton(factory => factory.GetInstance().PartialViewsFileSystem, "PartialViewFileSystem"); + composition.Container.RegisterSingleton(factory => factory.GetInstance().MacroPartialsFileSystem, "PartialViewMacroFileSystem"); + composition.Container.RegisterSingleton(factory => factory.GetInstance().StylesheetsFileSystem, "StylesheetFileSystem"); + composition.Container.RegisterSingleton(factory => factory.GetInstance().MasterPagesFileSystem, "MasterpageFileSystem"); + composition.Container.RegisterSingleton(factory => factory.GetInstance().MvcViewsFileSystem, "ViewFileSystem"); // register manifest builder, will be injected in eg PropertyEditorCollectionBuilder composition.Container.RegisterSingleton(factory diff --git a/src/Umbraco.Core/DI/Current.cs b/src/Umbraco.Core/DI/Current.cs index 00a1b5c784..36323afe61 100644 --- a/src/Umbraco.Core/DI/Current.cs +++ b/src/Umbraco.Core/DI/Current.cs @@ -3,6 +3,7 @@ using LightInject; using Umbraco.Core.Cache; using Umbraco.Core.Configuration; using Umbraco.Core.Dictionary; +using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Plugins; @@ -75,6 +76,9 @@ namespace Umbraco.Core.DI set { _pluginManager = value; } } + public static FileSystems FileSystems + => Container.GetInstance(); + public static UrlSegmentProviderCollection UrlSegmentProviders => Container.GetInstance(); diff --git a/src/Umbraco.Core/DI/RepositoryCompositionRoot.cs b/src/Umbraco.Core/DI/RepositoryCompositionRoot.cs index 82b51f8305..638ca84656 100644 --- a/src/Umbraco.Core/DI/RepositoryCompositionRoot.cs +++ b/src/Umbraco.Core/DI/RepositoryCompositionRoot.cs @@ -29,14 +29,6 @@ namespace Umbraco.Core.DI // register repository factory container.RegisterSingleton(); - // register file systems - container.RegisterSingleton(factory => new PhysicalFileSystem(SystemDirectories.Scripts), "ScriptFileSystem"); - container.RegisterSingleton(factory => new PhysicalFileSystem(SystemDirectories.MvcViews + "/Partials/"), "PartialViewFileSystem"); - container.RegisterSingleton(factory => new PhysicalFileSystem(SystemDirectories.MvcViews + "/MacroPartials/"), "PartialViewMacroFileSystem"); - container.RegisterSingleton(factory => new PhysicalFileSystem(SystemDirectories.Css), "StylesheetFileSystem"); - container.RegisterSingleton(factory => new PhysicalFileSystem(SystemDirectories.Masterpages), "MasterpageFileSystem"); - container.RegisterSingleton(factory => new PhysicalFileSystem(SystemDirectories.MvcViews), "ViewFileSystem"); - // register cache helpers // the main cache helper is registered by CoreBootManager and is used by most repositories // the disabled one is used by those repositories that have an annotated ctor parameter diff --git a/src/Umbraco.Core/DatabaseContext.cs b/src/Umbraco.Core/DatabaseContext.cs index 9a1061ebef..33a6871421 100644 --- a/src/Umbraco.Core/DatabaseContext.cs +++ b/src/Umbraco.Core/DatabaseContext.cs @@ -456,14 +456,7 @@ namespace Umbraco.Core var schemaResult = ValidateDatabaseSchema(); var installedSchemaVersion = new SemVersion(schemaResult.DetermineInstalledVersion()); - - var installedMigrationVersion = new SemVersion(0); - //we cannot check the migrations table if it doesn't exist, this will occur when upgrading to 7.3 - if (schemaResult.ValidTables.Any(x => x.InvariantEquals("umbracoMigration"))) - { - installedMigrationVersion = schemaResult.DetermineInstalledVersionByMigrations(migrationEntryService); - } - + var installedMigrationVersion = schemaResult.DetermineInstalledVersionByMigrations(migrationEntryService); var targetVersion = UmbracoVersion.Current; //In some cases - like upgrading from 7.2.6 -> 7.3, there will be no migration information in the database and therefore it will diff --git a/src/Umbraco.Core/Events/MigrationEventArgs.cs b/src/Umbraco.Core/Events/MigrationEventArgs.cs index 55c9eef331..5590ed4d26 100644 --- a/src/Umbraco.Core/Events/MigrationEventArgs.cs +++ b/src/Umbraco.Core/Events/MigrationEventArgs.cs @@ -128,8 +128,14 @@ namespace Umbraco.Core.Events get { return TargetSemVersion.GetVersion(); } } + /// + /// Gets the origin version of the migration, i.e. the one that is currently installed. + /// public SemVersion ConfiguredSemVersion { get; private set; } + /// + /// Gets the target version of the migration. + /// public SemVersion TargetSemVersion { get; private set; } public string ProductName { get; private set; } diff --git a/src/Umbraco.Core/IO/FileSystemExtensions.cs b/src/Umbraco.Core/IO/FileSystemExtensions.cs index 64dcfc25a0..aeb6f9a42a 100644 --- a/src/Umbraco.Core/IO/FileSystemExtensions.cs +++ b/src/Umbraco.Core/IO/FileSystemExtensions.cs @@ -37,15 +37,17 @@ namespace Umbraco.Core.IO throw new ArgumentException("Retries must be greater than zero"); } + // GetSize has been added to IFileSystem2 but not IFileSystem + // this is implementing GetSize for IFileSystem, the old way public static long GetSize(this IFileSystem fs, string path) { + // if we reach this point, fs is *not* IFileSystem2 + // so it's not FileSystemWrapper nor shadow nor anything we know + // so... fall back to the old & inefficient method + using (var file = fs.OpenFile(path)) { - using (var sr = new StreamReader(file)) - { - var str = sr.ReadToEnd(); - return str.Length; - } + return file.Length; } } diff --git a/src/Umbraco.Core/IO/FileSystemProviderManager.cs b/src/Umbraco.Core/IO/FileSystemProviderManager.cs deleted file mode 100644 index 6d72a13090..0000000000 --- a/src/Umbraco.Core/IO/FileSystemProviderManager.cs +++ /dev/null @@ -1,132 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Configuration; -using System.Linq; -using System.Reflection; -using System.Text; -using Umbraco.Core.CodeAnnotations; -using Umbraco.Core.Configuration; -using Umbraco.Core.Plugins; - -namespace Umbraco.Core.IO -{ - public class FileSystemProviderManager - { - private readonly FileSystemProvidersSection _config; - - #region Singleton - - private static readonly FileSystemProviderManager Instance = new FileSystemProviderManager(); - - public static FileSystemProviderManager Current - { - get { return Instance; } - } - - #endregion - - #region Constructors - - internal FileSystemProviderManager() - { - _config = (FileSystemProvidersSection)ConfigurationManager.GetSection("umbracoConfiguration/FileSystemProviders"); - } - - #endregion - - /// - /// used to cache the lookup of how to construct this object so we don't have to reflect each time. - /// - private class ProviderConstructionInfo - { - public object[] Parameters { get; set; } - public ConstructorInfo Constructor { get; set; } - public string ProviderAlias { get; set; } - } - - private readonly ConcurrentDictionary _providerLookup = new ConcurrentDictionary(); - private readonly ConcurrentDictionary _wrappedProviderLookup = new ConcurrentDictionary(); - - /// - /// Returns the underlying (non-typed) file system provider for the alias specified - /// - /// - /// - /// - /// It is recommended to use the typed GetFileSystemProvider method instead to get a strongly typed provider instance. - /// - public IFileSystem GetUnderlyingFileSystemProvider(string alias) - { - //either get the constructor info from cache or create it and add to cache - var ctorInfo = _providerLookup.GetOrAdd(alias, s => - { - var providerConfig = _config.Providers[s]; - if (providerConfig == null) - throw new ArgumentException(string.Format("No provider found with the alias '{0}'", s)); - - var providerType = Type.GetType(providerConfig.Type); - if (providerType == null) - throw new InvalidOperationException(string.Format("Could not find type '{0}'", providerConfig.Type)); - - if (providerType.IsAssignableFrom(typeof (IFileSystem))) - throw new InvalidOperationException(string.Format("The type '{0}' does not implement IFileSystem", providerConfig.Type)); - - var paramCount = providerConfig.Parameters != null ? providerConfig.Parameters.Count : 0; - var constructor = providerType.GetConstructors() - .SingleOrDefault(x => x.GetParameters().Count() == paramCount - && x.GetParameters().All(y => providerConfig.Parameters.AllKeys.Contains(y.Name))); - if (constructor == null) - throw new InvalidOperationException(string.Format("Could not find constructor for type '{0}' which accepts {1} parameters", providerConfig.Type, paramCount)); - - var parameters = new object[paramCount]; - for (var i = 0; i < paramCount; i++) - parameters[i] = providerConfig.Parameters[providerConfig.Parameters.AllKeys[i]].Value; - - //return the new constructor info class to cache so we don't have to do this again. - return new ProviderConstructionInfo() - { - Constructor = constructor, - Parameters = parameters, - ProviderAlias = s - }; - }); - - var fs = (IFileSystem)ctorInfo.Constructor.Invoke(ctorInfo.Parameters); - return fs; - } - - /// - /// Returns the strongly typed file system provider - /// - /// - /// - public TProviderTypeFilter GetFileSystemProvider() - where TProviderTypeFilter : FileSystemWrapper - { - //get the alias for the type from cache or look it up and add it to the cache, then we don't have to reflect each time - var alias = _wrappedProviderLookup.GetOrAdd(typeof (TProviderTypeFilter), fsType => - { - //validate the ctor - var constructor = fsType.GetConstructors() - .SingleOrDefault(x => - x.GetParameters().Count() == 1 && TypeHelper.IsTypeAssignableFrom(x.GetParameters().Single().ParameterType)); - if (constructor == null) - throw new InvalidOperationException("The type of " + fsType + " must inherit from FileSystemWrapper and must have a constructor that accepts one parameter of type " + typeof(IFileSystem)); - - var attr = - (FileSystemProviderAttribute)fsType.GetCustomAttributes(typeof(FileSystemProviderAttribute), false). - SingleOrDefault(); - - if (attr == null) - throw new InvalidOperationException(string.Format("The provider type filter '{0}' is missing the required FileSystemProviderAttribute", typeof(FileSystemProviderAttribute).FullName)); - - return attr.Alias; - }); - - var innerFs = GetUnderlyingFileSystemProvider(alias); - var outputFs = Activator.CreateInstance(typeof (TProviderTypeFilter), innerFs); - return (TProviderTypeFilter)outputFs; - } - } -} diff --git a/src/Umbraco.Core/IO/FileSystemWrapper.cs b/src/Umbraco.Core/IO/FileSystemWrapper.cs index ba2ad8f48b..27e08330ed 100644 --- a/src/Umbraco.Core/IO/FileSystemWrapper.cs +++ b/src/Umbraco.Core/IO/FileSystemWrapper.cs @@ -14,93 +14,100 @@ namespace Umbraco.Core.IO /// /// This abstract class just wraps the 'real' IFileSystem object passed in to its constructor. /// - public abstract class FileSystemWrapper : IFileSystem + public abstract class FileSystemWrapper : IFileSystem2 { - private readonly IFileSystem _wrapped; - - protected FileSystemWrapper(IFileSystem wrapped) + protected FileSystemWrapper(IFileSystem wrapped) { - _wrapped = wrapped; + Wrapped = wrapped; } - public IEnumerable GetDirectories(string path) + internal IFileSystem Wrapped { get; set; } + + public IEnumerable GetDirectories(string path) { - return _wrapped.GetDirectories(path); + return Wrapped.GetDirectories(path); } public void DeleteDirectory(string path) { - _wrapped.DeleteDirectory(path); + Wrapped.DeleteDirectory(path); } public void DeleteDirectory(string path, bool recursive) { - _wrapped.DeleteDirectory(path, recursive); + Wrapped.DeleteDirectory(path, recursive); } public bool DirectoryExists(string path) { - return _wrapped.DirectoryExists(path); + return Wrapped.DirectoryExists(path); } public void AddFile(string path, Stream stream) { - _wrapped.AddFile(path, stream); + Wrapped.AddFile(path, stream); } - public void AddFile(string path, Stream stream, bool overrideIfExists) + public void AddFile(string path, Stream stream, bool overrideExisting) { - _wrapped.AddFile(path, stream, overrideIfExists); + Wrapped.AddFile(path, stream, overrideExisting); } public IEnumerable GetFiles(string path) { - return _wrapped.GetFiles(path); + return Wrapped.GetFiles(path); } public IEnumerable GetFiles(string path, string filter) { - return _wrapped.GetFiles(path, filter); + return Wrapped.GetFiles(path, filter); } public Stream OpenFile(string path) { - return _wrapped.OpenFile(path); + return Wrapped.OpenFile(path); } public void DeleteFile(string path) { - _wrapped.DeleteFile(path); + Wrapped.DeleteFile(path); } public bool FileExists(string path) { - return _wrapped.FileExists(path); + return Wrapped.FileExists(path); } public string GetRelativePath(string fullPathOrUrl) { - return _wrapped.GetRelativePath(fullPathOrUrl); + return Wrapped.GetRelativePath(fullPathOrUrl); } public string GetFullPath(string path) { - return _wrapped.GetFullPath(path); + return Wrapped.GetFullPath(path); } public string GetUrl(string path) { - return _wrapped.GetUrl(path); + return Wrapped.GetUrl(path); } public DateTimeOffset GetLastModified(string path) { - return _wrapped.GetLastModified(path); + return Wrapped.GetLastModified(path); } public DateTimeOffset GetCreated(string path) { - return _wrapped.GetCreated(path); + return Wrapped.GetCreated(path); } - } + + // explicitely implementing - not breaking + long IFileSystem2.GetSize(string path) + { + var wrapped2 = Wrapped as IFileSystem2; + return wrapped2 == null ? Wrapped.GetSize(path) : wrapped2.GetSize(path); + } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/IO/FileSystems.cs b/src/Umbraco.Core/IO/FileSystems.cs new file mode 100644 index 0000000000..bedd51ec07 --- /dev/null +++ b/src/Umbraco.Core/IO/FileSystems.cs @@ -0,0 +1,254 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Configuration; +using System.Linq; +using System.Reflection; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; +using Umbraco.Core.Plugins; + +namespace Umbraco.Core.IO +{ + public class FileSystems + { + private readonly FileSystemProvidersSection _config; + private readonly WeakSet _wrappers = new WeakSet(); + private readonly ILogger _logger; + + // wrappers for shadow support + private readonly ShadowWrapper _macroPartialFileSystemWrapper; + private readonly ShadowWrapper _partialViewsFileSystemWrapper; + private readonly ShadowWrapper _stylesheetsFileSystemWrapper; + private readonly ShadowWrapper _scriptsFileSystemWrapper; + private readonly ShadowWrapper _xsltFileSystemWrapper; + private readonly ShadowWrapper _masterPagesFileSystemWrapper; + private readonly ShadowWrapper _mvcViewsFileSystemWrapper; + + #region Singleton & Constructor + + internal FileSystems(ILogger logger) + { + _config = (FileSystemProvidersSection) ConfigurationManager.GetSection("umbracoConfiguration/FileSystemProviders"); + _logger = logger; + + // create the filesystems + MacroPartialsFileSystem = new PhysicalFileSystem(SystemDirectories.MacroPartials); + PartialViewsFileSystem = new PhysicalFileSystem(SystemDirectories.PartialViews); + StylesheetsFileSystem = new PhysicalFileSystem(SystemDirectories.Css); + ScriptsFileSystem = new PhysicalFileSystem(SystemDirectories.Scripts); + XsltFileSystem = new PhysicalFileSystem(SystemDirectories.Xslt); + MasterPagesFileSystem = new PhysicalFileSystem(SystemDirectories.Masterpages); + MvcViewsFileSystem = new PhysicalFileSystem(SystemDirectories.MvcViews); + + // wrap the filesystems for shadow support + MacroPartialsFileSystem = _macroPartialFileSystemWrapper = new ShadowWrapper(MacroPartialsFileSystem, "Views/MacroPartials"); + PartialViewsFileSystem = _partialViewsFileSystemWrapper = new ShadowWrapper(PartialViewsFileSystem, "Views/Partials"); + StylesheetsFileSystem = _stylesheetsFileSystemWrapper = new ShadowWrapper(StylesheetsFileSystem, "css"); + ScriptsFileSystem = _scriptsFileSystemWrapper = new ShadowWrapper(ScriptsFileSystem, "scripts"); + XsltFileSystem = _xsltFileSystemWrapper = new ShadowWrapper(XsltFileSystem, "xslt"); + MasterPagesFileSystem = _masterPagesFileSystemWrapper = new ShadowWrapper(MasterPagesFileSystem, "masterpages"); + MvcViewsFileSystem = _mvcViewsFileSystemWrapper = new ShadowWrapper(MvcViewsFileSystem, "Views"); + + // obtain filesystems from GetFileSystem + // these are already wrapped and do not need to be wrapped again + MediaFileSystem = GetFileSystem(); + } + + #endregion + + #region Well-Known FileSystems + + public IFileSystem2 MacroPartialsFileSystem { get; } + public IFileSystem2 PartialViewsFileSystem { get; } + public IFileSystem2 StylesheetsFileSystem { get; } + public IFileSystem2 ScriptsFileSystem { get; } + public IFileSystem2 XsltFileSystem { get; } + public IFileSystem2 MasterPagesFileSystem { get; } + public IFileSystem2 MvcViewsFileSystem { get; } + public MediaFileSystem MediaFileSystem { get; } + + #endregion + + #region Providers + + /// + /// used to cache the lookup of how to construct this object so we don't have to reflect each time. + /// + private class ProviderConstructionInfo + { + public object[] Parameters { get; set; } + public ConstructorInfo Constructor { get; set; } + //public string ProviderAlias { get; set; } + } + + private readonly ConcurrentDictionary _providerLookup = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _aliases = new ConcurrentDictionary(); + + /// + /// Gets an underlying (non-typed) filesystem supporting a strongly-typed filesystem. + /// + /// The alias of the strongly-typed filesystem. + /// The non-typed filesystem supporting the strongly-typed filesystem with the specified alias. + /// This method should not be used directly, used instead. + internal IFileSystem GetUnderlyingFileSystemProvider(string alias) + { + // either get the constructor info from cache or create it and add to cache + var ctorInfo = _providerLookup.GetOrAdd(alias, s => + { + // get config + var providerConfig = _config.Providers[s]; + if (providerConfig == null) + throw new ArgumentException($"No provider found with alias {s}."); + + // get the filesystem type + var providerType = Type.GetType(providerConfig.Type); + if (providerType == null) + throw new InvalidOperationException($"Could not find type {providerConfig.Type}."); + + // ensure it implements IFileSystem + if (providerType.IsAssignableFrom(typeof (IFileSystem))) + throw new InvalidOperationException($"Type {providerType.FullName} does not implement IFileSystem."); + + // find a ctor matching the config parameters + var paramCount = providerConfig.Parameters?.Count ?? 0; + var constructor = providerType.GetConstructors().SingleOrDefault(x + => x.GetParameters().Length == paramCount && x.GetParameters().All(y => providerConfig.Parameters.AllKeys.Contains(y.Name))); + if (constructor == null) + throw new InvalidOperationException($"Type {providerType.FullName} has no ctor matching the {paramCount} configuration parameter(s)."); + + var parameters = new object[paramCount]; + if (providerConfig.Parameters != null) // keeps ReSharper happy + for (var i = 0; i < paramCount; i++) + parameters[i] = providerConfig.Parameters[providerConfig.Parameters.AllKeys[i]].Value; + + return new ProviderConstructionInfo + { + Constructor = constructor, + Parameters = parameters, + //ProviderAlias = s + }; + }); + + // create the fs and return + return (IFileSystem) ctorInfo.Constructor.Invoke(ctorInfo.Parameters); + } + + /// + /// Gets a strongly-typed filesystem. + /// + /// The type of the filesystem. + /// A strongly-typed filesystem of the specified type. + public TFileSystem GetFileSystem() + where TFileSystem : FileSystemWrapper + { + // deal with known types - avoid infinite loops! + if (typeof(TFileSystem) == typeof(MediaFileSystem) && MediaFileSystem != null) + return MediaFileSystem as TFileSystem; // else create and return + + // get/cache the alias for the filesystem type + var alias = _aliases.GetOrAdd(typeof (TFileSystem), fsType => + { + // validate the ctor + var constructor = fsType.GetConstructors().SingleOrDefault(x + => x.GetParameters().Length == 1 && TypeHelper.IsTypeAssignableFrom(x.GetParameters().Single().ParameterType)); + if (constructor == null) + throw new InvalidOperationException("Type " + fsType.FullName + " must inherit from FileSystemWrapper and have a constructor that accepts one parameter of type " + typeof(IFileSystem).FullName + "."); + + // find the attribute and get the alias + var attr = (FileSystemProviderAttribute) fsType.GetCustomAttributes(typeof(FileSystemProviderAttribute), false).SingleOrDefault(); + if (attr == null) + throw new InvalidOperationException("Type " + fsType.FullName + "is missing the required FileSystemProviderAttribute."); + + return attr.Alias; + }); + + // gets the inner fs, create the strongly-typed fs wrapping the inner fs, register & return + // so we are double-wrapping here + // could be optimized by having FileSystemWrapper inherit from ShadowWrapper, maybe + var innerFs = GetUnderlyingFileSystemProvider(alias); + var shadowWrapper = new ShadowWrapper(innerFs, "typed/" + alias); + var fs = (TFileSystem) Activator.CreateInstance(typeof (TFileSystem), innerFs); + _wrappers.Add(shadowWrapper); // keeping a weak reference to the wrapper + return fs; + } + + #endregion + + #region Shadow + + // note + // shadowing is thread-safe, but entering and exiting shadow mode is not, and there is only one + // global shadow for the entire application, so great care should be taken to ensure that the + // application is *not* doing anything else when using a shadow. + // shadow applies to well-known filesystems *only* - at the moment, any other filesystem that would + // be created directly (via ctor) or via GetFileSystem is *not* shadowed. + + // shadow must be enabled in an app event handler before anything else ie before any filesystem + // is actually created and used - after, it is too late - enabling shadow has a neglictible perfs + // impact. + // NO! by the time an app event handler is instanciated it is already too late, see note in ctor. + //internal void EnableShadow() + //{ + // if (_mvcViewsFileSystem != null) // test one of the fs... + // throw new InvalidOperationException("Cannot enable shadow once filesystems have been created."); + // _shadowEnabled = true; + //} + + public ShadowFileSystemsScope Shadow(Guid id) + { + var typed = _wrappers.ToArray(); + var wrappers = new ShadowWrapper[typed.Length + 7]; + var i = 0; + while (i < typed.Length) wrappers[i] = typed[i++]; + wrappers[i++] = _macroPartialFileSystemWrapper; + wrappers[i++] = _partialViewsFileSystemWrapper; + wrappers[i++] = _stylesheetsFileSystemWrapper; + wrappers[i++] = _scriptsFileSystemWrapper; + wrappers[i++] = _xsltFileSystemWrapper; + wrappers[i++] = _masterPagesFileSystemWrapper; + wrappers[i] = _mvcViewsFileSystemWrapper; + + return ShadowFileSystemsScope.CreateScope(id, wrappers, _logger); + } + + #endregion + + private class WeakSet + where T : class + { + private readonly HashSet> _set = new HashSet>(); + + public void Add(T item) + { + lock (_set) + { + _set.Add(new WeakReference(item)); + CollectLocked(); + } + } + + public T[] ToArray() + { + lock (_set) + { + CollectLocked(); + return _set.Select(x => + { + T target; + return x.TryGetTarget(out target) ? target : null; + }).WhereNotNull().ToArray(); + } + } + + private void CollectLocked() + { + _set.RemoveWhere(x => + { + T target; + return x.TryGetTarget(out target) == false; + }); + } + } + } +} diff --git a/src/Umbraco.Core/IO/IFileSystem.cs b/src/Umbraco.Core/IO/IFileSystem.cs index 3b38432c5c..063f63c437 100644 --- a/src/Umbraco.Core/IO/IFileSystem.cs +++ b/src/Umbraco.Core/IO/IFileSystem.cs @@ -16,7 +16,7 @@ namespace Umbraco.Core.IO void DeleteDirectory(string path, bool recursive); bool DirectoryExists(string path); - + void AddFile(string path, Stream stream); void AddFile(string path, Stream stream, bool overrideIfExists); @@ -31,7 +31,6 @@ namespace Umbraco.Core.IO bool FileExists(string path); - string GetRelativePath(string fullPathOrUrl); string GetFullPath(string path); @@ -42,4 +41,10 @@ namespace Umbraco.Core.IO DateTimeOffset GetCreated(string path); } + + // this should be part of IFileSystem but we don't want to change the interface + public interface IFileSystem2 : IFileSystem + { + long GetSize(string path); + } } diff --git a/src/Umbraco.Core/IO/IOHelper.cs b/src/Umbraco.Core/IO/IOHelper.cs index 4a69f8f127..96b9927978 100644 --- a/src/Umbraco.Core/IO/IOHelper.cs +++ b/src/Umbraco.Core/IO/IOHelper.cs @@ -4,14 +4,9 @@ using System.Globalization; using System.Reflection; using System.IO; using System.Configuration; -using System.Linq; using System.Web; -using System.Text.RegularExpressions; -using System.Threading.Tasks; using System.Web.Hosting; using ICSharpCode.SharpZipLib.Zip; -using Umbraco.Core.Configuration; -using Umbraco.Core.Logging; namespace Umbraco.Core.IO { @@ -20,17 +15,11 @@ namespace Umbraco.Core.IO private static string _rootDir = ""; // static compiled regex for faster performance - private readonly static Regex ResolveUrlPattern = new Regex("(=[\"\']?)(\\W?\\~(?:.(?![\"\']?\\s+(?:\\S+)=|[>\"\']))+.)[\"\']?", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); + //private static readonly Regex ResolveUrlPattern = new Regex("(=[\"\']?)(\\W?\\~(?:.(?![\"\']?\\s+(?:\\S+)=|[>\"\']))+.)[\"\']?", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); - public static char DirSepChar - { - get - { - return Path.DirectorySeparatorChar; - } - } + public static char DirSepChar => Path.DirectorySeparatorChar; - internal static void UnZip(string zipFilePath, string unPackDirectory, bool deleteZipFile) + internal static void UnZip(string zipFilePath, string unPackDirectory, bool deleteZipFile) { // Unzip string tempDir = unPackDirectory; @@ -359,51 +348,5 @@ namespace Umbraco.Core.IO writer.Write(contents); } } - - /// - /// Deletes all files passed in. - /// - /// - /// - /// - internal static bool DeleteFiles(IEnumerable files, Action onError = null) - { - //ensure duplicates are removed - files = files.Distinct(); - - var allsuccess = true; - - var fs = FileSystemProviderManager.Current.GetFileSystemProvider(); - Parallel.ForEach(files, file => - { - try - { - if (file.IsNullOrWhiteSpace()) return; - - var relativeFilePath = fs.GetRelativePath(file); - if (fs.FileExists(relativeFilePath) == false) return; - - var parentDirectory = Path.GetDirectoryName(relativeFilePath); - - // don't want to delete the media folder if not using directories. - if (UmbracoConfig.For.UmbracoSettings().Content.UploadAllowDirectories && parentDirectory != fs.GetRelativePath("/")) - { - //issue U4-771: if there is a parent directory the recursive parameter should be true - fs.DeleteDirectory(parentDirectory, String.IsNullOrEmpty(parentDirectory) == false); - } - else - { - fs.DeleteFile(file, true); - } - } - catch (Exception e) - { - onError?.Invoke(file, e); - allsuccess = false; - } - }); - - return allsuccess; - } } } diff --git a/src/Umbraco.Core/IO/MediaFileSystem.cs b/src/Umbraco.Core/IO/MediaFileSystem.cs index b35264d752..47c89628b3 100644 --- a/src/Umbraco.Core/IO/MediaFileSystem.cs +++ b/src/Umbraco.Core/IO/MediaFileSystem.cs @@ -1,9 +1,21 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; using System.Globalization; using System.IO; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.DI; +using Umbraco.Core.Exceptions; +using Umbraco.Core.Media; +using Umbraco.Core.Media.Exif; +using Umbraco.Core.Models; +using Umbraco.Core.Services; namespace Umbraco.Core.IO { @@ -14,59 +26,698 @@ namespace Umbraco.Core.IO public class MediaFileSystem : FileSystemWrapper { private readonly IContentSection _contentConfig; + private readonly ImageHelper _imageHelper; + private readonly UploadAutoFillProperties _uploadAutoFillProperties; + private readonly IDataTypeService _dataTypeService; - public MediaFileSystem(IFileSystem wrapped) + private readonly object _folderCounterLock = new object(); + private long _folderCounter; + private bool _folderCounterInitialized; + + private static readonly Dictionary DefaultSizes = new Dictionary + { + { 100, "thumb" }, + { 500, "big-thumb" } + }; + + // FIXME + // explain the two ctors + // fix the whole mediaHelper, imageHelper, autofill props mess + // also + // what shannon says: this is an "enhanced filesystem" so it's OK to use it with extra stuff! + + public MediaFileSystem(IFileSystem2 wrapped) : this(wrapped, UmbracoConfig.For.UmbracoSettings().Content) - { - } + { } - public MediaFileSystem(IFileSystem wrapped, IContentSection contentConfig) : base(wrapped) + public MediaFileSystem(IFileSystem2 wrapped, IContentSection contentConfig) : base(wrapped) { _contentConfig = contentConfig; + _imageHelper = new ImageHelper(contentConfig); // FIXME inject or KILL } - public string GetRelativePath(int propertyId, string fileName) + // note - this is currently experimental / being developed + //public static bool UseTheNewMediaPathScheme { get; set; } + public const bool UseTheNewMediaPathScheme = false; + + // none of the methods below are used in Core anymore + // fixme FIX THEM WTF WTF WTF?! + + [Obsolete("This low-level method should NOT exist.")] + public string GetRelativePath(int propertyId, string fileName) { - var seperator = _contentConfig.UploadAllowDirectories + var sep = _contentConfig.UploadAllowDirectories ? Path.DirectorySeparatorChar : '-'; - return propertyId.ToString(CultureInfo.InvariantCulture) + seperator + fileName; + return propertyId.ToString(CultureInfo.InvariantCulture) + sep + fileName; } + [Obsolete("This low-level method should NOT exist.", false)] public string GetRelativePath(string subfolder, string fileName) { - var seperator = _contentConfig.UploadAllowDirectories + var sep = _contentConfig.UploadAllowDirectories ? Path.DirectorySeparatorChar : '-'; - return subfolder + seperator + fileName; + return subfolder + sep + fileName; } - public IEnumerable GetThumbnails(string path) - { - var parentDirectory = Path.GetDirectoryName(path); - var extension = Path.GetExtension(path); + /// + /// Deletes all files passed in. + /// + /// + /// + /// + internal bool DeleteFiles(IEnumerable files, Action onError = null) + { + //ensure duplicates are removed + files = files.Distinct(); - return GetFiles(parentDirectory) - .Where(x => x.StartsWith(path.TrimEnd(extension) + "_thumb") || x.StartsWith(path.TrimEnd(extension) + "_big-thumb")) - .ToList(); - } + var allsuccess = true; + var rootRelativePath = GetRelativePath("/"); - public void DeleteFile(string path, bool deleteThumbnails) - { - DeleteFile(path); + Parallel.ForEach(files, file => + { + try + { + if (file.IsNullOrWhiteSpace()) return; - if (deleteThumbnails == false) - return; + var relativeFilePath = GetRelativePath(file); + if (FileExists(relativeFilePath) == false) return; - DeleteThumbnails(path); - } + var parentDirectory = Path.GetDirectoryName(relativeFilePath); - public void DeleteThumbnails(string path) - { - GetThumbnails(path) - .ForEach(DeleteFile); - } - } + // don't want to delete the media folder if not using directories. + if (_contentConfig.UploadAllowDirectories && parentDirectory != rootRelativePath) + { + //issue U4-771: if there is a parent directory the recursive parameter should be true + DeleteDirectory(parentDirectory, string.IsNullOrEmpty(parentDirectory) == false); + } + else + { + DeleteFile(file, true); + } + } + catch (Exception e) + { + onError?.Invoke(file, e); + allsuccess = false; + } + }); + + return allsuccess; + } + + #region Media Path + + /// + /// Gets the file path of a media file. + /// + /// The file name. + /// The unique identifier of the content/media owning the file. + /// The unique identifier of the property type owning the file. + /// The filesystem-relative path to the media file. + /// With the old media path scheme, this CREATES a new media path each time it is invoked. + public string GetMediaPath(string filename, Guid cuid, Guid puid) + { + filename = Path.GetFileName(filename); + if (filename == null) throw new ArgumentException("Cannot become a safe filename.", nameof(filename)); + filename = IOHelper.SafeFileName(filename.ToLowerInvariant()); + + string folder; + if (UseTheNewMediaPathScheme == false) + { + // old scheme: filepath is "/" OR "-" + // default media filesystem maps to "~/media/" + folder = GetNextFolder(); + } + else + { + // new scheme: path is "-/" OR "--" + // default media filesystem maps to "~/media/" + // assumes that cuid and puid keys can be trusted - and that a single property type + // for a single content cannot store two different files with the same name + folder = Combine(cuid, puid).ToHexString(/*'/', 2, 4*/); // could use ext to fragment path eg 12/e4/f2/... + } + + var filepath = UmbracoConfig.For.UmbracoSettings().Content.UploadAllowDirectories + ? Path.Combine(folder, filename) + : folder + "-" + filename; + + return filepath; + } + + private static byte[] Combine(Guid guid1, Guid guid2) + { + var bytes1 = guid1.ToByteArray(); + var bytes2 = guid2.ToByteArray(); + var bytes = new byte[bytes1.Length]; + for (var i = 0; i < bytes1.Length; i++) + bytes[i] = (byte) (bytes1[i] ^ bytes2[i]); + return bytes; + } + + /// + /// Gets the file path of a media file. + /// + /// The file name. + /// A previous file path. + /// The unique identifier of the content/media owning the file. + /// The unique identifier of the property type owning the file. + /// The filesystem-relative path to the media file. + /// In the old, legacy, number-based scheme, we try to re-use the media folder + /// specified by . Else, we CREATE a new one. Each time we are invoked. + public string GetMediaPath(string filename, string prevpath, Guid cuid, Guid puid) + { + if (UseTheNewMediaPathScheme || string.IsNullOrWhiteSpace(prevpath)) + return GetMediaPath(filename, cuid, puid); + + filename = Path.GetFileName(filename); + if (filename == null) throw new ArgumentException("Cannot become a safe filename.", nameof(filename)); + filename = IOHelper.SafeFileName(filename.ToLowerInvariant()); + + // old scheme, with a previous path + // prevpath should be "/" OR "-" + // and we want to reuse the "" part, so try to find it + + var sep = UmbracoConfig.For.UmbracoSettings().Content.UploadAllowDirectories ? "/" : "-"; + var pos = prevpath.IndexOf(sep, StringComparison.Ordinal); + var s = pos > 0 ? prevpath.Substring(0, pos) : null; + int ignored; + + var folder = (pos > 0 && int.TryParse(s, out ignored)) ? s : GetNextFolder(); + + // ReSharper disable once AssignNullToNotNullAttribute + var filepath = UmbracoConfig.For.UmbracoSettings().Content.UploadAllowDirectories + ? Path.Combine(folder, filename) + : folder + "-" + filename; + + return filepath; + } + + /// + /// Gets the next media folder in the original number-based scheme. + /// + /// + /// Should be private, is internal for legacy FileHandlerData which is obsolete. + internal string GetNextFolder() + { + EnsureFolderCounterIsInitialized(); + return Interlocked.Increment(ref _folderCounter).ToString(CultureInfo.InvariantCulture); + } + + private void EnsureFolderCounterIsInitialized() + { + lock (_folderCounterLock) + { + if (_folderCounterInitialized) return; + + _folderCounter = 1000; // seed + var directories = GetDirectories(""); + foreach (var directory in directories) + { + long folderNumber; + if (long.TryParse(directory, out folderNumber) && folderNumber > _folderCounter) + _folderCounter = folderNumber; + } + + // note: not multi-domains ie LB safe as another domain could create directories + // while we read and parse them - don't fix, move to new scheme eventually + + _folderCounterInitialized = true; + } + } + + #endregion + + #region Associated Media Files + + /// + /// Stores a media file associated to a property of a content item. + /// + /// The content item owning the media file. + /// The property type owning the media file. + /// The media file name. + /// A stream containing the media bytes. + /// An optional filesystem-relative filepath to the previous media file. + /// The filesystem-relative filepath to the media file. + /// + /// The file is considered "owned" by the content/propertyType. + /// If an is provided then that file (and associated thumbnails if any) is deleted + /// before the new file is saved, and depending on the media path scheme, the folder may be reused for the new file. + /// + public string StoreFile(IContentBase content, PropertyType propertyType, string filename, Stream filestream, string oldpath) + { + if (content == null) throw new ArgumentNullException(nameof(content)); + if (propertyType == null) throw new ArgumentNullException(nameof(propertyType)); + if (string.IsNullOrWhiteSpace(filename)) throw new ArgumentNullOrEmptyException(nameof(filename)); + if (filestream == null) throw new ArgumentNullException(nameof(filestream)); + + // clear the old file, if any + if (string.IsNullOrWhiteSpace(oldpath) == false) + DeleteFile(oldpath, true); + + // get the filepath, store the data + // use oldpath as "prevpath" to try and reuse the folder, in original number-based scheme + var filepath = GetMediaPath(filename, oldpath, content.Key, propertyType.Key); + AddFile(filepath, filestream); + return filepath; + } + + /// + /// Clears a media file. + /// + /// The filesystem-relative path to the media file. + public new void DeleteFile(string filepath) + { + DeleteFile(filepath, true); + } + + /// + /// Copies a media file as a new media file, associated to a property of a content item. + /// + /// The content item owning the copy of the media file. + /// The property type owning the copy of the media file. + /// The filesystem-relative path to the source media file. + /// The filesystem-relative path to the copy of the media file. + public string CopyFile(IContentBase content, PropertyType propertyType, string sourcepath) + { + if (content == null) throw new ArgumentNullException(nameof(content)); + if (propertyType == null) throw new ArgumentNullException(nameof(propertyType)); + if (string.IsNullOrWhiteSpace(sourcepath)) throw new ArgumentNullOrEmptyException(nameof(sourcepath)); + + // ensure we have a file to copy + if (FileExists(sourcepath) == false) return null; + + // get the filepath + var filename = Path.GetFileName(sourcepath); + var filepath = GetMediaPath(filename, content.Key, propertyType.Key); + this.CopyFile(sourcepath, filepath); + CopyThumbnails(sourcepath, filepath); + return filepath; + } + + // gets or creates a property for a content item. + private static Property GetProperty(IContentBase content, string propertyTypeAlias) + { + var property = content.Properties.FirstOrDefault(x => x.Alias.InvariantEquals(propertyTypeAlias)); + if (property != null) return property; + + var propertyType = content.GetContentType().CompositionPropertyTypes + .FirstOrDefault(x => x.Alias.InvariantEquals(propertyTypeAlias)); + if (propertyType == null) + throw new Exception("No property type exists with alias " + propertyTypeAlias + "."); + + property = new Property(propertyType); + content.Properties.Add(property); + return property; + } + + // fixme - what's below belongs to the upload property editor, not the media filesystem! + + public void SetUploadFile(IContentBase content, string propertyTypeAlias, string filename, Stream filestream) + { + var property = GetProperty(content, propertyTypeAlias); + var svalue = property.Value as string; + var oldpath = svalue == null ? null : GetRelativePath(svalue); + var filepath = StoreFile(content, property.PropertyType, filename, filestream, oldpath); + property.Value = GetUrl(filepath); + SetUploadFile(content, property, filepath, filestream); + } + + public void SetUploadFile(IContentBase content, string propertyTypeAlias, string filepath) + { + var property = GetProperty(content, propertyTypeAlias); + var svalue = property.Value as string; + var oldpath = svalue == null ? null : GetRelativePath(svalue); // FIXME DELETE? + if (string.IsNullOrWhiteSpace(oldpath) == false && oldpath != filepath) + DeleteFile(oldpath); + property.Value = GetUrl(filepath); + using (var filestream = OpenFile(filepath)) + { + SetUploadFile(content, property, filepath, filestream); + } + } + + // sets a file for the FileUpload property editor + // ie generates thumbnails and populates autofill properties + private void SetUploadFile(IContentBase content, Property property, string filepath, Stream filestream) + { + // check if file is an image (and supports resizing and thumbnails etc) + var extension = Path.GetExtension(filepath); + var isImage = _imageHelper.IsImageFile(extension); + + // specific stuff for images (thumbnails etc) + if (isImage) + { + using (var image = Image.FromStream(filestream)) + { + // use one image for all + GenerateThumbnails(image, filepath, property.PropertyType); + _uploadAutoFillProperties.Populate(content, property.Alias, filepath, filestream, image); + } + } + else + { + // will use filepath for extension, and filestream for length + _uploadAutoFillProperties.Populate(content, property.Alias, filepath, filestream); + } + } + + #endregion + + #region Image + + /// + /// Gets a value indicating whether the file extension corresponds to an image. + /// + /// The file extension. + /// A value indicating whether the file extension corresponds to an image. + public bool IsImageFile(string extension) + { + if (extension == null) return false; + extension = extension.TrimStart('.'); + return _contentConfig.ImageFileTypes.InvariantContains(extension); + } + + /// + /// Gets the dimensions of an image. + /// + /// A stream containing the image bytes. + /// The dimension of the image. + /// First try with EXIF as it is faster and does not load the entire image + /// in memory. Fallback to GDI which means loading the image in memory and thus + /// use potentially large amounts of memory. + public Size GetDimensions(Stream stream) + { + //Try to load with exif + try + { + var jpgInfo = ImageFile.FromStream(stream); + + if (jpgInfo.Format != ImageFileFormat.Unknown + && jpgInfo.Properties.ContainsKey(ExifTag.PixelYDimension) + && jpgInfo.Properties.ContainsKey(ExifTag.PixelXDimension)) + { + var height = Convert.ToInt32(jpgInfo.Properties[ExifTag.PixelYDimension].Value); + var width = Convert.ToInt32(jpgInfo.Properties[ExifTag.PixelXDimension].Value); + if (height > 0 && width > 0) + { + return new Size(width, height); + } + } + } + catch (Exception) + { + //We will just swallow, just means we can't read exif data, we don't want to log an error either + } + + //we have no choice but to try to read in via GDI + using (var image = Image.FromStream(stream)) + { + + var fileWidth = image.Width; + var fileHeight = image.Height; + return new Size(fileWidth, fileHeight); + } + } + + #endregion + + #region Manage thumbnails + + // note: this does not find 'custom' thumbnails? + // will find _thumb and _big-thumb but NOT _custom? + public IEnumerable GetThumbnails(string path) + { + var parentDirectory = Path.GetDirectoryName(path); + var extension = Path.GetExtension(path); + + return GetFiles(parentDirectory) + .Where(x => x.StartsWith(path.TrimEnd(extension) + "_thumb") || x.StartsWith(path.TrimEnd(extension) + "_big-thumb")) + .ToList(); + } + + public void DeleteFile(string path, bool deleteThumbnails) + { + DeleteFile(path); + + if (deleteThumbnails == false) + return; + + DeleteThumbnails(path); + } + + public void DeleteThumbnails(string path) + { + GetThumbnails(path) + .ForEach(DeleteFile); + } + + public void CopyThumbnails(string sourcePath, string targetPath) + { + var targetPathBase = Path.GetDirectoryName(targetPath) ?? ""; + foreach (var sourceThumbPath in GetThumbnails(sourcePath)) + { + var sourceThumbFilename = Path.GetFileName(sourceThumbPath) ?? ""; + var targetThumbPath = Path.Combine(targetPathBase, sourceThumbFilename); + this.CopyFile(sourceThumbPath, targetThumbPath); + } + } + + #endregion + + #region GenerateThumbnails + + public IEnumerable GenerateThumbnails(Image image, string filepath, string preValue) + { + if (string.IsNullOrWhiteSpace(preValue)) + return GenerateThumbnails(image, filepath); + + var additionalSizes = new List(); + var sep = preValue.Contains(",") ? "," : ";"; + var values = preValue.Split(new[] { sep }, StringSplitOptions.RemoveEmptyEntries); + foreach (var value in values) + { + int size; + if (int.TryParse(value, out size)) + additionalSizes.Add(size); + } + + return GenerateThumbnails(image, filepath, additionalSizes); + } + + public IEnumerable GenerateThumbnails(Image image, string filepath, IEnumerable additionalSizes = null) + { + var w = image.Width; + var h = image.Height; + + var sizes = additionalSizes == null ? DefaultSizes.Keys : DefaultSizes.Keys.Concat(additionalSizes); + + // start with default sizes, + // add additional sizes, + // filter out duplicates, + // filter out those that would be larger that the original image + // and create the thumbnail + return sizes + .Distinct() + .Where(x => w >= x && h >= x) + .Select(x => GenerateResized(image, filepath, DefaultSizes.ContainsKey(x) ? DefaultSizes[x] : "", x)) + .ToList(); // now + } + + public IEnumerable GenerateThumbnails(Stream filestream, string filepath, PropertyType propertyType) + { + // get the original image from the original stream + if (filestream.CanSeek) filestream.Seek(0, 0); // fixme - what if we cannot seek? + using (var image = Image.FromStream(filestream)) + { + return GenerateThumbnails(image, filepath, propertyType); + } + } + + public IEnumerable GenerateThumbnails(Image image, string filepath, PropertyType propertyType) + { + // if the editor is an upload field, check for additional thumbnail sizes + // that can be defined in the prevalue for the property data type. otherwise, + // just use the default sizes. + var sizes = propertyType.PropertyEditorAlias == Constants.PropertyEditors.UploadFieldAlias + ? _dataTypeService + .GetPreValuesByDataTypeId(propertyType.DataTypeDefinitionId) + .FirstOrDefault() + : string.Empty; + + return GenerateThumbnails(image, filepath, sizes); + } + + #endregion + + #region GenerateResized - Generate at resized filepath derived from origin filepath + + public ResizedImage GenerateResized(Image originImage, string originFilepath, string sizeName, int maxWidthHeight) + { + return GenerateResized(originImage, originFilepath, sizeName, maxWidthHeight, -1, -1); + } + + public ResizedImage GenerateResized(Image originImage, string originFilepath, string sizeName, int fixedWidth, int fixedHeight) + { + return GenerateResized(originImage, originFilepath, sizeName, -1, fixedWidth, fixedHeight); + } + + public ResizedImage GenerateResized(Image originImage, string originFilepath, string sizeName, int maxWidthHeight, int fixedWidth, int fixedHeight) + { + if (string.IsNullOrWhiteSpace(sizeName)) + sizeName = "UMBRACOSYSTHUMBNAIL"; + var extension = Path.GetExtension(originFilepath) ?? string.Empty; + var filebase = originFilepath.TrimEnd(extension); + var resizedFilepath = filebase + "_" + sizeName + extension; + + return GenerateResizedAt(originImage, resizedFilepath, maxWidthHeight, fixedWidth, fixedHeight); + } + + #endregion + + #region GenerateResizedAt - Generate at specified resized filepath + + public ResizedImage GenerateResizedAt(Image originImage, string resizedFilepath, int maxWidthHeight) + { + return GenerateResizedAt(originImage, resizedFilepath, maxWidthHeight, -1, -1); + } + + public ResizedImage GenerateResizedAt(Image originImage, int fixedWidth, int fixedHeight, string resizedFilepath) + { + return GenerateResizedAt(originImage, resizedFilepath, -1, fixedWidth, fixedHeight); + } + + public ResizedImage GenerateResizedAt(Image originImage, string resizedFilepath, int maxWidthHeight, int fixedWidth, int fixedHeight) + { + // target dimensions + int width, height; + + // if maxWidthHeight then get ratio + if (maxWidthHeight > 0) + { + var fx = (float)originImage.Size.Width / maxWidthHeight; + var fy = (float)originImage.Size.Height / maxWidthHeight; + var f = Math.Max(fx, fy); // fit in thumbnail size + width = (int)Math.Round(originImage.Size.Width / f); + height = (int)Math.Round(originImage.Size.Height / f); + if (width == 0) width = 1; + if (height == 0) height = 1; + } + else if (fixedWidth > 0 && fixedHeight > 0) + { + width = fixedWidth; + height = fixedHeight; + } + else + { + width = height = 1; + } + + // create new image with best quality settings + using (var bitmap = new Bitmap(width, height)) + using (var graphics = Graphics.FromImage(bitmap)) + { + // if the image size is rather large we cannot use the best quality interpolation mode + // because we'll get out of mem exceptions. So we detect how big the image is and use + // the mid quality interpolation mode when the image size exceeds our max limit. + graphics.InterpolationMode = originImage.Width > 5000 || originImage.Height > 5000 + ? InterpolationMode.Bilinear // mid quality + : InterpolationMode.HighQualityBicubic; // best quality + + // everything else is best-quality + graphics.SmoothingMode = SmoothingMode.HighQuality; + graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; + graphics.CompositingQuality = CompositingQuality.HighQuality; + + // copy the old image to the new and resize + var rect = new Rectangle(0, 0, width, height); + graphics.DrawImage(originImage, rect, 0, 0, originImage.Width, originImage.Height, GraphicsUnit.Pixel); + + // copy metadata + // fixme - er... no? + + // get an encoder - based upon the file type + var extension = (Path.GetExtension(resizedFilepath) ?? "").TrimStart('.').ToLowerInvariant(); + var encoders = ImageCodecInfo.GetImageEncoders(); + ImageCodecInfo encoder; + switch (extension) + { + case "png": + encoder = encoders.Single(t => t.MimeType.Equals("image/png")); + break; + case "gif": + encoder = encoders.Single(t => t.MimeType.Equals("image/gif")); + break; + case "tif": + case "tiff": + encoder = encoders.Single(t => t.MimeType.Equals("image/tiff")); + break; + case "bmp": + encoder = encoders.Single(t => t.MimeType.Equals("image/bmp")); + break; + // TODO: this is dirty, defaulting to jpg but the return value of this thing is used all over the + // place so left it here, but it needs to not set a codec if it doesn't know which one to pick + // Note: when fixing this: both .jpg and .jpeg should be handled as extensions + default: + encoder = encoders.Single(t => t.MimeType.Equals("image/jpeg")); + break; + } + + // set compresion ratio to 90% + var encoderParams = new EncoderParameters(); + encoderParams.Param[0] = new EncoderParameter(Encoder.Quality, 90L); + + // save the new image + using (var stream = new MemoryStream()) + { + bitmap.Save(stream, encoder, encoderParams); + stream.Seek(0, 0); + if (resizedFilepath.Contains("UMBRACOSYSTHUMBNAIL")) + { + var filepath = resizedFilepath.Replace("UMBRACOSYSTHUMBNAIL", maxWidthHeight.ToInvariantString()); + AddFile(filepath, stream); + if (extension != "jpg") + { + filepath = filepath.TrimEnd(extension) + "jpg"; + stream.Seek(0, 0); + AddFile(filepath, stream); + } + // TODO: Remove this, this is ONLY here for backwards compatibility but it is essentially completely unusable see U4-5385 + stream.Seek(0, 0); + resizedFilepath = resizedFilepath.Replace("UMBRACOSYSTHUMBNAIL", width + "x" + height); + } + + AddFile(resizedFilepath, stream); + } + + return new ResizedImage(resizedFilepath, width, height); + } + } + + #endregion + + #region Inner classes + + public class ResizedImage + { + public ResizedImage() + { } + + public ResizedImage(string filepath, int width, int height) + { + Filepath = filepath; + Width = width; + Height = height; + } + + public string Filepath { get; set; } + public int Width { get; set; } + public int Height { get; set; } + } + + #endregion + } } diff --git a/src/Umbraco.Core/IO/PhysicalFileSystem.cs b/src/Umbraco.Core/IO/PhysicalFileSystem.cs index 107ce71957..d2640ac66e 100644 --- a/src/Umbraco.Core/IO/PhysicalFileSystem.cs +++ b/src/Umbraco.Core/IO/PhysicalFileSystem.cs @@ -8,29 +8,32 @@ using Umbraco.Core.Logging; namespace Umbraco.Core.IO { - public class PhysicalFileSystem : IFileSystem + public class PhysicalFileSystem : IFileSystem2 { // the rooted, filesystem path, using directory separator chars, NOT ending with a separator // eg "c:" or "c:\path\to\site" or "\\server\path" private readonly string _rootPath; - // the ??? url, using url separator chars, NOT ending with a separator - // eg "" (?) or "/Scripts" or ??? + // _rootPath, but with separators replaced by forward-slashes + // eg "c:" or "c:/path/to/site" or "//server/path" + // (is used in GetRelativePath) + private readonly string _rootPathFwd; + + // the relative url, using url separator chars, NOT ending with a separator + // eg "" or "/Views" or "/Media" or "//Media" in case of a virtual path private readonly string _rootUrl; + // virtualRoot should be "~/path/to/root" eg "~/Views" + // the "~/" is mandatory. public PhysicalFileSystem(string virtualRoot) { if (virtualRoot == null) throw new ArgumentNullException("virtualRoot"); if (virtualRoot.StartsWith("~/") == false) throw new ArgumentException("The virtualRoot argument must be a virtual path and start with '~/'"); - _rootPath = IOHelper.MapPath(virtualRoot); - _rootPath = EnsureDirectorySeparatorChar(_rootPath); - _rootPath = _rootPath.TrimEnd(Path.DirectorySeparatorChar); - - _rootUrl = IOHelper.ResolveUrl(virtualRoot); - _rootUrl = EnsureUrlSeparatorChar(_rootUrl); - _rootUrl = _rootUrl.TrimEnd('/'); + _rootPath = EnsureDirectorySeparatorChar(IOHelper.MapPath(virtualRoot)).TrimEnd(Path.DirectorySeparatorChar); + _rootPathFwd = EnsureUrlSeparatorChar(_rootPath); + _rootUrl = EnsureUrlSeparatorChar(IOHelper.ResolveUrl(virtualRoot)).TrimEnd('/'); } public PhysicalFileSystem(string rootPath, string rootUrl) @@ -40,22 +43,24 @@ namespace Umbraco.Core.IO if (rootPath.StartsWith("~/")) throw new ArgumentException("The rootPath argument cannot be a virtual path and cannot start with '~/'"); // rootPath should be... rooted, as in, it's a root path! - // but the test suite App.config cannot really "root" anything so we'll have to do it here - - //var localRoot = AppDomain.CurrentDomain.BaseDirectory; - var localRoot = IOHelper.GetRootDirectorySafe(); if (Path.IsPathRooted(rootPath) == false) { + // but the test suite App.config cannot really "root" anything so we have to do it here + var localRoot = IOHelper.GetRootDirectorySafe(); rootPath = Path.Combine(localRoot, rootPath); } - rootPath = EnsureDirectorySeparatorChar(rootPath); - rootUrl = EnsureUrlSeparatorChar(rootUrl); - - _rootPath = rootPath.TrimEnd(Path.DirectorySeparatorChar); - _rootUrl = rootUrl.TrimEnd('/'); + _rootPath = EnsureDirectorySeparatorChar(rootPath).TrimEnd(Path.DirectorySeparatorChar); + _rootPathFwd = EnsureUrlSeparatorChar(_rootPath); + _rootUrl = EnsureUrlSeparatorChar(rootUrl).TrimEnd('/'); } + /// + /// Gets directories in a directory. + /// + /// The filesystem-relative path to the directory. + /// The filesystem-relative path to the directories in the directory. + /// Filesystem-relative paths use forward-slashes as directory separators. public IEnumerable GetDirectories(string path) { var fullPath = GetFullPath(path); @@ -77,11 +82,20 @@ namespace Umbraco.Core.IO return Enumerable.Empty(); } + /// + /// Deletes a directory. + /// + /// The filesystem-relative path of the directory. public void DeleteDirectory(string path) { DeleteDirectory(path, false); } + /// + /// Deletes a directory. + /// + /// The filesystem-relative path of the directory. + /// A value indicating whether to recursively delete sub-directories. public void DeleteDirectory(string path, bool recursive) { var fullPath = GetFullPath(path); @@ -98,38 +112,71 @@ namespace Umbraco.Core.IO } } + /// + /// Gets a value indicating whether a directory exists. + /// + /// The filesystem-relative path of the directory. + /// A value indicating whether a directory exists. public bool DirectoryExists(string path) { var fullPath = GetFullPath(path); return Directory.Exists(fullPath); } + /// + /// Saves a file. + /// + /// The filesystem-relative path of the file. + /// A stream containing the file data. + /// Overrides the existing file, if any. public void AddFile(string path, Stream stream) { AddFile(path, stream, true); } - public void AddFile(string path, Stream stream, bool overrideIfExists) + /// + /// Saves a file. + /// + /// The filesystem-relative path of the file. + /// A stream containing the file data. + /// A value indicating whether to override the existing file, if any. + /// If a file exists and is false, an exception is thrown. + public void AddFile(string path, Stream stream, bool overrideExisting) { var fullPath = GetFullPath(path); var exists = File.Exists(fullPath); - if (exists && overrideIfExists == false) + if (exists && overrideExisting == false) throw new InvalidOperationException(string.Format("A file at path '{0}' already exists", path)); - Directory.CreateDirectory(Path.GetDirectoryName(fullPath)); // ensure it exists + var directory = Path.GetDirectoryName(fullPath); + if (directory == null) throw new InvalidOperationException("Could not get directory."); + Directory.CreateDirectory(directory); // ensure it exists - if (stream.CanSeek) + if (stream.CanSeek) // fixme - what else? stream.Seek(0, 0); - using (var destination = (Stream)File.Create(fullPath)) + using (var destination = (Stream) File.Create(fullPath)) stream.CopyTo(destination); } + /// + /// Gets files in a directory. + /// + /// The filesystem-relative path of the directory. + /// The filesystem-relative path to the files in the directory. + /// Filesystem-relative paths use forward-slashes as directory separators. public IEnumerable GetFiles(string path) { return GetFiles(path, "*.*"); } + /// + /// Gets files in a directory. + /// + /// The filesystem-relative path of the directory. + /// A filter. + /// The filesystem-relative path to the matching files in the directory. + /// Filesystem-relative paths use forward-slashes as directory separators. public IEnumerable GetFiles(string path, string filter) { var fullPath = GetFullPath(path); @@ -151,12 +198,21 @@ namespace Umbraco.Core.IO return Enumerable.Empty(); } + /// + /// Opens a file. + /// + /// The filesystem-relative path to the file. + /// public Stream OpenFile(string path) { var fullPath = GetFullPath(path); return File.OpenRead(fullPath); } + /// + /// Deletes a file. + /// + /// The filesystem-relative path to the file. public void DeleteFile(string path) { var fullPath = GetFullPath(path); @@ -173,52 +229,53 @@ namespace Umbraco.Core.IO } } + /// + /// Gets a value indicating whether a file exists. + /// + /// The filesystem-relative path to the file. + /// A value indicating whether the file exists. public bool FileExists(string path) { var fullpath = GetFullPath(path); return File.Exists(fullpath); } - // beware, many things depend on how the GetRelative/AbsolutePath methods work! - /// - /// Gets the relative path. + /// Gets the filesystem-relative path of a full path or of an url. /// /// The full path or url. /// The path, relative to this filesystem's root. /// /// The relative path is relative to this filesystem's root, not starting with any - /// directory separator. If input was recognized as a url (path), then output uses url (path) separator - /// chars. + /// directory separator. All separators are forward-slashes. /// public string GetRelativePath(string fullPathOrUrl) { // test url var path = fullPathOrUrl.Replace('\\', '/'); // ensure url separator char - if (IOHelper.PathStartsWith(path, _rootUrl, '/')) // if it starts with the root url... - return path.Substring(_rootUrl.Length) // strip it - .TrimStart('/'); // it's relative + // if it starts with the root url, strip it and trim the starting slash to make it relative + // eg "/Media/1234/img.jpg" => "1234/img.jpg" + if (IOHelper.PathStartsWith(path, _rootUrl, '/')) + return path.Substring(_rootUrl.Length).TrimStart('/'); - // test path - path = EnsureDirectorySeparatorChar(fullPathOrUrl); + // if it starts with the root path, strip it and trim the starting slash to make it relative + // eg "c:/websites/test/root/Media/1234/img.jpg" => "1234/img.jpg" + if (IOHelper.PathStartsWith(path, _rootPathFwd, '/')) + return path.Substring(_rootPathFwd.Length).TrimStart('/'); - if (IOHelper.PathStartsWith(path, _rootPath, Path.DirectorySeparatorChar)) // if it starts with the root path - return path.Substring(_rootPath.Length) // strip it - .TrimStart(Path.DirectorySeparatorChar); // it's relative - - // unchanged - including separators - return fullPathOrUrl; + // unchanged - what else? + return path; } /// /// Gets the full path. /// - /// The full or relative path. + /// The full or filesystem-relative path. /// The full path. /// /// On the physical filesystem, the full path is the rooted (ie non-relative), safe (ie within this - /// filesystem's root) path. All separators are converted to Path.DirectorySeparatorChar. + /// filesystem's root) path. All separators are Path.DirectorySeparatorChar. /// public string GetFullPath(string path) { @@ -226,49 +283,82 @@ namespace Umbraco.Core.IO var opath = path; path = EnsureDirectorySeparatorChar(path); + // fixme - this part should go! // not sure what we are doing here - so if input starts with a (back) slash, // we assume it's not a FS relative path and we try to convert it... but it // really makes little sense? if (path.StartsWith(Path.DirectorySeparatorChar.ToString())) path = GetRelativePath(path); - // if already a full path, return - if (IOHelper.PathStartsWith(path, _rootPath, Path.DirectorySeparatorChar)) - return path; + // if not already rooted, combine with the root path + if (IOHelper.PathStartsWith(path, _rootPath, Path.DirectorySeparatorChar) == false) + path = Path.Combine(_rootPath, path); - // else combine and sanitize, ie GetFullPath will take care of any relative + // sanitize - GetFullPath will take care of any relative // segments in path, eg '../../foo.tmp' - it may throw a SecurityException // if the combined path reaches illegal parts of the filesystem - var fpath = Path.Combine(_rootPath, path); - fpath = Path.GetFullPath(fpath); + path = Path.GetFullPath(path); // at that point, path is within legal parts of the filesystem, ie we have // permissions to reach that path, but it may nevertheless be outside of // our root path, due to relative segments, so better check - if (IOHelper.PathStartsWith(fpath, _rootPath, Path.DirectorySeparatorChar)) - return fpath; + if (IOHelper.PathStartsWith(path, _rootPath, Path.DirectorySeparatorChar)) + return path; + // nothing prevents us to reach the file, security-wise, yet it is outside + // this filesystem's root - throw throw new FileSecurityException("File '" + opath + "' is outside this filesystem's root."); } + /// + /// Gets the url. + /// + /// The filesystem-relative path. + /// The url. + /// All separators are forward-slashes. public string GetUrl(string path) { path = EnsureUrlSeparatorChar(path).Trim('/'); return _rootUrl + "/" + path; } + /// + /// Gets the last-modified date of a directory or file. + /// + /// The filesystem-relative path to the directory or the file. + /// The last modified date of the directory or the file. public DateTimeOffset GetLastModified(string path) { - return DirectoryExists(path) - ? new DirectoryInfo(GetFullPath(path)).LastWriteTimeUtc - : new FileInfo(GetFullPath(path)).LastWriteTimeUtc; + var fullpath = GetFullPath(path); + return DirectoryExists(fullpath) + ? new DirectoryInfo(fullpath).LastWriteTimeUtc + : new FileInfo(fullpath).LastWriteTimeUtc; } + /// + /// Gets the created date of a directory or file. + /// + /// The filesystem-relative path to the directory or the file. + /// The created date of the directory or the file. public DateTimeOffset GetCreated(string path) { - return DirectoryExists(path) - ? Directory.GetCreationTimeUtc(GetFullPath(path)) - : File.GetCreationTimeUtc(GetFullPath(path)); + var fullpath = GetFullPath(path); + return DirectoryExists(fullpath) + ? Directory.GetCreationTimeUtc(fullpath) + : File.GetCreationTimeUtc(fullpath); + } + + /// + /// Gets the size of a file. + /// + /// The filesystem-relative path to the file. + /// The file of the size, in bytes. + /// If the file does not exist, returns -1. + public long GetSize(string path) + { + var fullPath = GetFullPath(path); + var file = new FileInfo(fullPath); + return file.Exists ? file.Length : -1; } #region Helper Methods diff --git a/src/Umbraco.Core/IO/ResizedImage.cs b/src/Umbraco.Core/IO/ResizedImage.cs deleted file mode 100644 index 6586699ecf..0000000000 --- a/src/Umbraco.Core/IO/ResizedImage.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Umbraco.Core.IO -{ - internal class ResizedImage - { - public ResizedImage() - { - } - - public ResizedImage(int width, int height, string fileName) - { - Width = width; - Height = height; - FileName = fileName; - } - - public int Width { get; set; } - public int Height { get; set; } - public string FileName { get; set; } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/IO/ShadowFileSystem.cs b/src/Umbraco.Core/IO/ShadowFileSystem.cs new file mode 100644 index 0000000000..276e058bcc --- /dev/null +++ b/src/Umbraco.Core/IO/ShadowFileSystem.cs @@ -0,0 +1,286 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Umbraco.Core.IO +{ + public class ShadowFileSystem : IFileSystem2 + { + private readonly IFileSystem _fs; + private readonly IFileSystem2 _sfs; + + public ShadowFileSystem(IFileSystem fs, IFileSystem2 sfs) + { + _fs = fs; + _sfs = sfs; + } + + public IFileSystem Inner + { + get { return _fs; } + } + + public void Complete() + { + if (_nodes == null) return; + var exceptions = new List(); + foreach (var kvp in _nodes) + { + if (kvp.Value.IsExist) + { + if (kvp.Value.IsFile) + { + try + { + using (var stream = _sfs.OpenFile(kvp.Key)) + _fs.AddFile(kvp.Key, stream, true); + } + catch (Exception e) + { + exceptions.Add(new Exception("Could not save file \"" + kvp.Key + "\".", e)); + } + } + } + else + { + try + { + if (kvp.Value.IsDir) + _fs.DeleteDirectory(kvp.Key, true); + else + _fs.DeleteFile(kvp.Key); + } + catch (Exception e) + { + exceptions.Add(new Exception("Could not delete " + (kvp.Value.IsDir ? "directory": "file") + " \"" + kvp.Key + "\".", e)); + } + } + } + _nodes.Clear(); + + if (exceptions.Count == 0) return; + throw new AggregateException("Failed to apply all changes (see exceptions).", exceptions); + } + + private Dictionary _nodes; + + private Dictionary Nodes { get { return _nodes ?? (_nodes = new Dictionary()); } } + + private class ShadowNode + { + public ShadowNode(bool isDelete, bool isdir) + { + IsDelete = isDelete; + IsDir = isdir; + } + + public bool IsDelete { get; private set; } + public bool IsDir { get; private set; } + + public bool IsExist { get { return IsDelete == false; } } + public bool IsFile { get { return IsDir == false; } } + } + + private static string NormPath(string path) + { + return path.ToLowerInvariant().Replace("\\", "/"); + } + + // values can be "" (root), "foo", "foo/bar"... + private static bool IsChild(string path, string input) + { + if (input.StartsWith(path) == false || input.Length < path.Length + 2) + return false; + if (path.Length > 0 && input[path.Length] != '/') return false; + var pos = input.IndexOf("/", path.Length + 1, StringComparison.OrdinalIgnoreCase); + return pos < 0; + } + + private static bool IsDescendant(string path, string input) + { + if (input.StartsWith(path) == false || input.Length < path.Length + 2) + return false; + return path.Length == 0 || input[path.Length] == '/'; + } + + public IEnumerable GetDirectories(string path) + { + var normPath = NormPath(path); + var shadows = Nodes.Where(kvp => IsChild(normPath, kvp.Key)).ToArray(); + var directories = _fs.GetDirectories(path); + return directories + .Except(shadows.Where(kvp => (kvp.Value.IsDir && kvp.Value.IsDelete) || (kvp.Value.IsFile && kvp.Value.IsExist)) + .Select(kvp => kvp.Key)) + .Union(shadows.Where(kvp => kvp.Value.IsDir && kvp.Value.IsExist).Select(kvp => kvp.Key)) + .Distinct(); + } + + public void DeleteDirectory(string path) + { + DeleteDirectory(path, false); + } + + public void DeleteDirectory(string path, bool recursive) + { + if (DirectoryExists(path) == false) return; + var normPath = NormPath(path); + if (recursive) + { + Nodes[normPath] = new ShadowNode(true, true); + var remove = Nodes.Where(x => IsDescendant(normPath, x.Key)).ToList(); + foreach (var kvp in remove) Nodes.Remove(kvp.Key); + Delete(path, true); + } + else + { + if (Nodes.Any(x => IsChild(normPath, x.Key) && x.Value.IsExist) // shadow content + || _fs.GetDirectories(path).Any() || _fs.GetFiles(path).Any()) // actual content + throw new InvalidOperationException("Directory is not empty."); + Nodes[path] = new ShadowNode(true, true); + var remove = Nodes.Where(x => IsChild(normPath, x.Key)).ToList(); + foreach (var kvp in remove) Nodes.Remove(kvp.Key); + Delete(path, false); + } + } + + private void Delete(string path, bool recurse) + { + foreach (var file in _fs.GetFiles(path)) + { + Nodes[NormPath(file)] = new ShadowNode(true, false); + } + foreach (var dir in _fs.GetDirectories(path)) + { + Nodes[NormPath(dir)] = new ShadowNode(true, true); + if (recurse) Delete(dir, true); + } + } + + public bool DirectoryExists(string path) + { + ShadowNode sf; + if (Nodes.TryGetValue(NormPath(path), out sf)) + return sf.IsDir && sf.IsExist; + return _fs.DirectoryExists(path); + } + + public void AddFile(string path, Stream stream) + { + AddFile(path, stream, true); + } + + public void AddFile(string path, Stream stream, bool overrideIfExists) + { + ShadowNode sf; + var normPath = NormPath(path); + if (Nodes.TryGetValue(normPath, out sf) && sf.IsExist && (sf.IsDir || overrideIfExists == false)) + throw new InvalidOperationException(string.Format("A file at path '{0}' already exists", path)); + + var parts = normPath.Split('/'); + for (var i = 0; i < parts.Length - 1; i++) + { + var dirPath = string.Join("/", parts.Take(i + 1)); + ShadowNode sd; + if (Nodes.TryGetValue(dirPath, out sd)) + { + if (sd.IsFile) throw new InvalidOperationException("Invalid path."); + if (sd.IsDelete) Nodes[dirPath] = new ShadowNode(false, true); + } + else + { + if (_fs.DirectoryExists(dirPath)) continue; + if (_fs.FileExists(dirPath)) throw new InvalidOperationException("Invalid path."); + Nodes[dirPath] = new ShadowNode(false, true); + } + } + + _sfs.AddFile(path, stream, overrideIfExists); + Nodes[normPath] = new ShadowNode(false, false); + } + + public IEnumerable GetFiles(string path) + { + var normPath = NormPath(path); + var shadows = Nodes.Where(kvp => IsChild(normPath, kvp.Key)).ToArray(); + var files = _fs.GetFiles(path); + return files + .Except(shadows.Where(kvp => (kvp.Value.IsFile && kvp.Value.IsDelete) || kvp.Value.IsDir) + .Select(kvp => kvp.Key)) + .Union(shadows.Where(kvp => kvp.Value.IsFile && kvp.Value.IsExist).Select(kvp => kvp.Key)) + .Distinct(); + } + + public IEnumerable GetFiles(string path, string filter) + { + return _fs.GetFiles(path, filter); + } + + public Stream OpenFile(string path) + { + ShadowNode sf; + if (Nodes.TryGetValue(NormPath(path), out sf)) + return sf.IsDir || sf.IsDelete ? null : _sfs.OpenFile(path); + return _fs.OpenFile(path); + } + + public void DeleteFile(string path) + { + if (FileExists(path) == false) return; + Nodes[NormPath(path)] = new ShadowNode(true, false); + } + + public bool FileExists(string path) + { + ShadowNode sf; + if (Nodes.TryGetValue(NormPath(path), out sf)) + return sf.IsFile && sf.IsExist; + return _fs.FileExists(path); + } + + public string GetRelativePath(string fullPathOrUrl) + { + return _fs.GetRelativePath(fullPathOrUrl); + } + + public string GetFullPath(string path) + { + return _fs.GetFullPath(path); + } + + public string GetUrl(string path) + { + return _fs.GetUrl(path); + } + + public DateTimeOffset GetLastModified(string path) + { + ShadowNode sf; + if (Nodes.TryGetValue(NormPath(path), out sf) == false) return _fs.GetLastModified(path); + if (sf.IsDelete) throw new InvalidOperationException("Invalid path."); + return _sfs.GetLastModified(path); + } + + public DateTimeOffset GetCreated(string path) + { + ShadowNode sf; + if (Nodes.TryGetValue(NormPath(path), out sf) == false) return _fs.GetCreated(path); + if (sf.IsDelete) throw new InvalidOperationException("Invalid path."); + return _sfs.GetCreated(path); + } + + public long GetSize(string path) + { + ShadowNode sf; + if (Nodes.TryGetValue(NormPath(path), out sf) == false) + { + // the inner filesystem (_fs) can be IFileSystem2... or just IFileSystem + // figure it out and use the most effective GetSize method + var fs2 = _fs as IFileSystem2; + return fs2 == null ? _fs.GetSize(path) : fs2.GetSize(path); + } + if (sf.IsDelete || sf.IsDir) throw new InvalidOperationException("Invalid path."); + return _sfs.GetSize(path); + } + } +} diff --git a/src/Umbraco.Core/IO/ShadowFileSystemsScope.cs b/src/Umbraco.Core/IO/ShadowFileSystemsScope.cs new file mode 100644 index 0000000000..da710cfb2b --- /dev/null +++ b/src/Umbraco.Core/IO/ShadowFileSystemsScope.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Remoting.Messaging; +using Umbraco.Core.Logging; + +namespace Umbraco.Core.IO +{ + public class ShadowFileSystemsScope : IDisposable + { + // note: taking a reference to the _manager instead of using manager.Current + // to avoid using Current everywhere but really, we support only 1 scope at + // a time, not multiple scopes in case of multiple managers (not supported) + + private const string ItemKey = "Umbraco.Core.IO.ShadowFileSystemsScope"; + private static readonly object Locker = new object(); + private readonly Guid _id; + private readonly ShadowWrapper[] _wrappers; + private readonly ILogger _logger; + + static ShadowFileSystemsScope() + { + SafeCallContext.Register( + () => + { + var scope = CallContext.LogicalGetData(ItemKey); + CallContext.FreeNamedDataSlot(ItemKey); + return scope; + }, + o => + { + if (CallContext.LogicalGetData(ItemKey) != null) throw new InvalidOperationException(); + if (o != null) CallContext.LogicalSetData(ItemKey, o); + }); + } + + private ShadowFileSystemsScope(Guid id, ShadowWrapper[] wrappers, ILogger logger) + { + _logger.Debug("Shadow " + id + "."); + _id = id; + _wrappers = wrappers; + _logger = logger; + foreach (var wrapper in _wrappers) + wrapper.Shadow(id); + } + + // internal for tests + FileSystems + // do NOT use otherwise + internal static ShadowFileSystemsScope CreateScope(Guid id, ShadowWrapper[] wrappers, ILogger logger) + { + lock (Locker) + { + if (CallContext.LogicalGetData(ItemKey) != null) throw new InvalidOperationException("Already shadowing."); + CallContext.LogicalSetData(ItemKey, ItemKey); // value does not matter + } + return new ShadowFileSystemsScope(id, wrappers, logger); + } + + internal static bool InScope => NoScope == false; + + internal static bool NoScope => CallContext.LogicalGetData(ItemKey) == null; + + public void Complete() + { + lock (Locker) + { + _logger.Debug("UnShadow " + _id + " (complete)."); + + var exceptions = new List(); + foreach (var wrapper in _wrappers) + { + try + { + // this may throw an AggregateException if some of the changes could not be applied + wrapper.UnShadow(true); + } + catch (AggregateException ae) + { + exceptions.Add(ae); + } + } + + if (exceptions.Count > 0) + throw new AggregateException("Failed to apply all changes (see exceptions).", exceptions); + + // last, & *only* if successful (otherwise we'll unshadow & cleanup as best as we can) + CallContext.FreeNamedDataSlot(ItemKey); + } + } + + public void Dispose() + { + lock (Locker) + { + if (CallContext.LogicalGetData(ItemKey) == null) return; + + try + { + _logger.Debug("UnShadow " + _id + " (abort)"); + foreach (var wrapper in _wrappers) + wrapper.UnShadow(false); // should not throw + } + finally + { + // last, & always + CallContext.FreeNamedDataSlot(ItemKey); + } + } + } + } +} diff --git a/src/Umbraco.Core/IO/ShadowWrapper.cs b/src/Umbraco.Core/IO/ShadowWrapper.cs new file mode 100644 index 0000000000..7f9bc61a66 --- /dev/null +++ b/src/Umbraco.Core/IO/ShadowWrapper.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Web.Hosting; + +namespace Umbraco.Core.IO +{ + public class ShadowWrapper : IFileSystem2 + { + private readonly IFileSystem _innerFileSystem; + private readonly string _shadowPath; + private ShadowFileSystem _shadowFileSystem; + private string _shadowDir; + + public ShadowWrapper(IFileSystem innerFileSystem, string shadowPath) + { + _innerFileSystem = innerFileSystem; + _shadowPath = shadowPath; + } + + internal void Shadow(Guid id) + { + // note: no thread-safety here, because ShadowFs is thread-safe due to the check + // on ShadowFileSystemsScope.None - and if None is false then we should be running + // in a single thread anyways + + var virt = "~/App_Data/Shadow/" + id + "/" + _shadowPath; + _shadowDir = IOHelper.MapPath(virt); + Directory.CreateDirectory(_shadowDir); + var tempfs = new PhysicalFileSystem(virt); + _shadowFileSystem = new ShadowFileSystem(_innerFileSystem, tempfs); + } + + internal void UnShadow(bool complete) + { + var shadowFileSystem = _shadowFileSystem; + var dir = _shadowDir; + _shadowFileSystem = null; + _shadowDir = null; + + try + { + // this may throw an AggregateException if some of the changes could not be applied + if (complete) shadowFileSystem.Complete(); + } + finally + { + // in any case, cleanup + try + { + Directory.Delete(dir, true); + dir = dir.Substring(0, dir.Length - _shadowPath.Length - 1); + if (Directory.EnumerateFileSystemEntries(dir).Any() == false) + Directory.Delete(dir, true); + } + catch + { + // ugly, isn't it? but if we cannot cleanup, bah, just leave it there + } + } + } + + private IFileSystem FileSystem + { + get { return ShadowFileSystemsScope.NoScope ? _innerFileSystem : _shadowFileSystem; } + } + + public IEnumerable GetDirectories(string path) + { + return FileSystem.GetDirectories(path); + } + + public void DeleteDirectory(string path) + { + FileSystem.DeleteDirectory(path); + } + + public void DeleteDirectory(string path, bool recursive) + { + FileSystem.DeleteDirectory(path, recursive); + } + + public bool DirectoryExists(string path) + { + return FileSystem.DirectoryExists(path); + } + + public void AddFile(string path, Stream stream) + { + FileSystem.AddFile(path, stream); + } + + public void AddFile(string path, Stream stream, bool overrideExisting) + { + FileSystem.AddFile(path, stream, overrideExisting); + } + + public IEnumerable GetFiles(string path) + { + return FileSystem.GetFiles(path); + } + + public IEnumerable GetFiles(string path, string filter) + { + return FileSystem.GetFiles(path, filter); + } + + public Stream OpenFile(string path) + { + return FileSystem.OpenFile(path); + } + + public void DeleteFile(string path) + { + FileSystem.DeleteFile(path); + } + + public bool FileExists(string path) + { + return FileSystem.FileExists(path); + } + + public string GetRelativePath(string fullPathOrUrl) + { + return FileSystem.GetRelativePath(fullPathOrUrl); + } + + public string GetFullPath(string path) + { + return FileSystem.GetFullPath(path); + } + + public string GetUrl(string path) + { + return FileSystem.GetUrl(path); + } + + public DateTimeOffset GetLastModified(string path) + { + return FileSystem.GetLastModified(path); + } + + public DateTimeOffset GetCreated(string path) + { + return FileSystem.GetCreated(path); + } + + public long GetSize(string path) + { + var filesystem = FileSystem; // will be either a ShadowFileSystem OR the actual underlying IFileSystem + + // and the underlying filesystem can be IFileSystem2... or just IFileSystem + // figure it out and use the most effective GetSize method + var filesystem2 = filesystem as IFileSystem2; + return filesystem2 == null ? filesystem.GetSize(path) : filesystem2.GetSize(path); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/IO/SystemDirectories.cs b/src/Umbraco.Core/IO/SystemDirectories.cs index ec055f21e2..be04e7f94e 100644 --- a/src/Umbraco.Core/IO/SystemDirectories.cs +++ b/src/Umbraco.Core/IO/SystemDirectories.cs @@ -96,7 +96,23 @@ namespace Umbraco.Core.IO } } - + public static string PartialViews + { + get + { + return MvcViews + "/Partials/"; + } + } + + public static string MacroPartials + { + get + { + return MvcViews + "/MacroPartials/"; + + } + } + public static string Media { get diff --git a/src/Umbraco.Core/IO/UmbracoMediaFile.cs b/src/Umbraco.Core/IO/UmbracoMediaFile.cs deleted file mode 100644 index 83942f339d..0000000000 --- a/src/Umbraco.Core/IO/UmbracoMediaFile.cs +++ /dev/null @@ -1,223 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Drawing.Drawing2D; -using System.Drawing.Imaging; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using System.Web; -using Umbraco.Core.Configuration; -using Umbraco.Core.DI; -using Umbraco.Core.Logging; -using Umbraco.Core.Media; - -namespace Umbraco.Core.IO -{ - public class UmbracoMediaFile - { - private readonly MediaFileSystem _fs; - - #region Constructors - - public UmbracoMediaFile() - { - _fs = FileSystemProviderManager.Current.GetFileSystemProvider(); - } - - public UmbracoMediaFile(string path) - { - _fs = FileSystemProviderManager.Current.GetFileSystemProvider(); - - Path = path; - - Initialize(); - } - - #endregion - - #region Static Methods - - //MB: Do we really need all these overloads? looking through the code, only one of them is actually used - - public static UmbracoMediaFile Save(HttpPostedFile file, string path) - { - return Save(file.InputStream, path); - } - - public static UmbracoMediaFile Save(HttpPostedFileBase file, string path) - { - return Save(file.InputStream, path); - } - - public static UmbracoMediaFile Save(Stream inputStream, string path) - { - var fs = FileSystemProviderManager.Current.GetFileSystemProvider(); - fs.AddFile(path, inputStream); - - return new UmbracoMediaFile(path); - } - - public static UmbracoMediaFile Save(byte[] file, string relativePath) - { - return Save(new MemoryStream(file), relativePath); - } - - public static UmbracoMediaFile Save(HttpPostedFile file) - { - var tempDir = System.IO.Path.Combine("uploads", Guid.NewGuid().ToString()); - return Save(file, tempDir); - } - - //filebase overload... - public static UmbracoMediaFile Save(HttpPostedFileBase file) - { - var tempDir = System.IO.Path.Combine("uploads", Guid.NewGuid().ToString()); - return Save(file, tempDir); - } - - #endregion - - private long? _length; - private Size? _size; - - /// - /// Initialized values that don't require opening the file. - /// - private void Initialize() - { - Filename = _fs.GetFileName(Path); - Extension = _fs.GetExtension(Path) != null - ? _fs.GetExtension(Path).Substring(1).ToLowerInvariant() - : ""; - Url = _fs.GetUrl(Path); - Exists = _fs.FileExists(Path); - if (Exists == false) - { - Current.Logger.Warn("The media file doesn't exist: " + Path); - } - } - - public bool Exists { get; private set; } - - public string Filename { get; private set; } - - public string Extension { get; private set; } - - public string Path { get; private set; } - - public string Url { get; private set; } - - /// - /// Get the length of the file in bytes - /// - /// - /// We are lazy loading this, don't go opening the file on ctor like we were doing. - /// - public long Length - { - get - { - if (_length == null) - { - if (Exists) - { - _length = _fs.GetSize(Path); - } - else - { - _length = -1; - } - } - return _length.Value; - } - } - - public bool SupportsResizing - { - get - { - return UmbracoConfig.For.UmbracoSettings().Content.ImageFileTypes.InvariantContains(Extension); - } - } - - public string GetFriendlyName() - { - return Filename.SplitPascalCasing().ToFirstUpperInvariant(); - } - - public Size GetDimensions() - { - if (_size == null) - { - if (_fs.FileExists(Path)) - { - EnsureFileSupportsResizing(); - - using (var fs = _fs.OpenFile(Path)) - { - _size = ImageHelper.GetDimensions(fs); - } - } - else - { - _size = new Size(-1, -1); - } - } - return _size.Value; - } - - public string Resize(int width, int height) - { - if (Exists) - { - EnsureFileSupportsResizing(); - - var fileNameThumb = DoResize(width, height, -1, string.Empty); - - return _fs.GetUrl(fileNameThumb); - } - return string.Empty; - } - - public string Resize(int maxWidthHeight, string fileNameAddition) - { - if (Exists) - { - EnsureFileSupportsResizing(); - - var fileNameThumb = DoResize(-1, -1, maxWidthHeight, fileNameAddition); - - return _fs.GetUrl(fileNameThumb); - } - return string.Empty; - } - - private string DoResize(int width, int height, int maxWidthHeight, string fileNameAddition) - { - using (var fs = _fs.OpenFile(Path)) - { - using (var image = Image.FromStream(fs)) - { - var fileNameThumb = string.IsNullOrWhiteSpace(fileNameAddition) - ? string.Format("{0}_UMBRACOSYSTHUMBNAIL." + Extension, Path.Substring(0, Path.LastIndexOf(".", StringComparison.Ordinal))) - : string.Format("{0}_{1}." + Extension, Path.Substring(0, Path.LastIndexOf(".", StringComparison.Ordinal)), fileNameAddition); - - var thumbnail = maxWidthHeight == -1 - ? ImageHelper.GenerateThumbnail(image, width, height, fileNameThumb, Extension, _fs) - : ImageHelper.GenerateThumbnail(image, maxWidthHeight, fileNameThumb, Extension, _fs); - - return thumbnail.FileName; - } - } - } - - private void EnsureFileSupportsResizing() - { - if (SupportsResizing == false) - throw new InvalidOperationException(string.Format("The file {0} is not an image, so can't get dimensions", Filename)); - } - - - } -} diff --git a/src/Umbraco.Core/IO/ViewHelper.cs b/src/Umbraco.Core/IO/ViewHelper.cs index e321bfa35c..4772edcf9a 100644 --- a/src/Umbraco.Core/IO/ViewHelper.cs +++ b/src/Umbraco.Core/IO/ViewHelper.cs @@ -2,7 +2,6 @@ using System; using System.IO; using System.Linq; using System.Text; -using System.Text.RegularExpressions; using Umbraco.Core.Models; namespace Umbraco.Core.IO @@ -13,19 +12,36 @@ namespace Umbraco.Core.IO public ViewHelper(IFileSystem viewFileSystem) { - if (viewFileSystem == null) throw new ArgumentNullException("viewFileSystem"); + if (viewFileSystem == null) throw new ArgumentNullException(nameof(viewFileSystem)); _viewFileSystem = viewFileSystem; } internal bool ViewExists(ITemplate t) { return _viewFileSystem.FileExists(ViewPath(t.Alias)); - } + } + + internal string GetFileContents(ITemplate t) + { + var viewContent = ""; + var path = ViewPath(t.Alias); + + if (_viewFileSystem.FileExists(path)) + { + using (var tr = new StreamReader(_viewFileSystem.OpenFile(path))) + { + viewContent = tr.ReadToEnd(); + tr.Close(); + } + } + + return viewContent; + } public string CreateView(ITemplate t, bool overWrite = false) { string viewContent; - string path = ViewPath(t.Alias); + var path = ViewPath(t.Alias); if (_viewFileSystem.FileExists(path) == false || overWrite) { @@ -138,18 +154,14 @@ namespace Umbraco.Core.IO public string ViewPath(string alias) { return _viewFileSystem.GetRelativePath(alias.Replace(" ", "") + ".cshtml"); - - //return SystemDirectories.MvcViews + "/" + alias.Replace(" ", "") + ".cshtml"; } - private string EnsureInheritedLayout(ITemplate template) + private static string EnsureInheritedLayout(ITemplate template) { - string design = template.Content; + var design = template.Content; if (string.IsNullOrEmpty(design)) - { design = GetDefaultFileContent(template.MasterTemplateAlias); - } return design; } diff --git a/src/Umbraco.Core/Logging/DebugDiagnosticsLogger.cs b/src/Umbraco.Core/Logging/DebugDiagnosticsLogger.cs index 8f6a07e75e..1c65ac26e1 100644 --- a/src/Umbraco.Core/Logging/DebugDiagnosticsLogger.cs +++ b/src/Umbraco.Core/Logging/DebugDiagnosticsLogger.cs @@ -6,7 +6,7 @@ namespace Umbraco.Core.Logging /// /// Implements on top of . /// - internal class DebugDiagnosticsLogger : ILogger + public class DebugDiagnosticsLogger : ILogger { /// public void Error(Type reporting, string message, Exception exception = null) diff --git a/src/Umbraco.Core/Logging/VoidProfiler.cs b/src/Umbraco.Core/Logging/VoidProfiler.cs new file mode 100644 index 0000000000..a33f1b2444 --- /dev/null +++ b/src/Umbraco.Core/Logging/VoidProfiler.cs @@ -0,0 +1,31 @@ +using System; + +namespace Umbraco.Core.Logging +{ + internal class VoidProfiler : IProfiler + { + private readonly VoidDisposable _disposable = new VoidDisposable(); + + public string Render() + { + return string.Empty; + } + + public IDisposable Step(string name) + { + return _disposable; + } + + public void Start() + { } + + public void Stop(bool discardResults = false) + { } + + private class VoidDisposable : DisposableObject + { + protected override void DisposeResources() + { } + } + } +} diff --git a/src/Umbraco.Core/Logging/WebProfiler.cs b/src/Umbraco.Core/Logging/WebProfiler.cs index fd8925bc1d..4f78aee9d7 100644 --- a/src/Umbraco.Core/Logging/WebProfiler.cs +++ b/src/Umbraco.Core/Logging/WebProfiler.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Web; using StackExchange.Profiling; using StackExchange.Profiling.SqlFormatters; @@ -10,29 +11,56 @@ namespace Umbraco.Core.Logging /// internal class WebProfiler : IProfiler { - private readonly IRuntimeState _runtime; + private const string BootRequestItemKey = "Umbraco.Core.Logging.WebProfiler__isBootRequest"; + private readonly WebProfilerProvider _provider; + private int _first; - public WebProfiler(IRuntimeState runtime) + public WebProfiler() { - _runtime = runtime; - } + // create our own provider, which can provide a profiler even during boot + _provider = new WebProfilerProvider(); - public void UmbracoApplicationEndRequest(object sender, EventArgs e) - { - if (CanPerformProfilingAction(sender)) - Stop(); + // settings + MiniProfiler.Settings.SqlFormatter = new SqlServerFormatter(); + MiniProfiler.Settings.StackMaxLength = 5000; + MiniProfiler.Settings.ProfilerProvider = _provider; } public void UmbracoApplicationBeginRequest(object sender, EventArgs e) { - if (CanPerformProfilingAction(sender)) + // if this is the first request, notify our own provider that this request is the boot request + var first = Interlocked.Exchange(ref _first, 1) == 0; + if (first) + { + _provider.BeginBootRequest(); + ((HttpApplication) sender).Context.Items[BootRequestItemKey] = true; + // and no need to start anything, profiler is already there + } + // else start a profiler, the normal way + else if (ShouldProfile(sender)) Start(); } - private static bool CanPerformProfilingAction(object sender) + public void UmbracoApplicationEndRequest(object sender, EventArgs e) + { + // if this is the boot request, or if we should profile this request, stop + // (the boot request is always profiled, no matter what) + var isBootRequest = ((HttpApplication) sender).Context.Items[BootRequestItemKey] != null; // fixme perfs + if (isBootRequest) + _provider.EndBootRequest(); + if (isBootRequest || ShouldProfile(sender)) + Stop(); + } + + private static bool ShouldProfile(object sender) { var request = TryGetRequest(sender); - return request.Success && request.Result.Url.IsClientSideRequest() == false; + if (request.Success == false) return false; + + if (request.Result.Url.IsClientSideRequest()) return false; + if (string.IsNullOrEmpty(request.Result.QueryString["umbDebug"])) return false; + if (request.Result.Url.IsBackOfficeRequest(HttpRuntime.AppDomainAppVirtualPath)) return false; + return true; } /// @@ -44,14 +72,12 @@ namespace Umbraco.Core.Logging /// public IDisposable Step(string name) { - return _runtime.Debug ? MiniProfiler.Current.Step(name) : null; + return MiniProfiler.Current.Step(name); } /// public void Start() { - MiniProfiler.Settings.SqlFormatter = new SqlServerFormatter(); - MiniProfiler.Settings.StackMaxLength = 5000; MiniProfiler.Start(); } diff --git a/src/Umbraco.Core/Logging/WebProfilerComponent.cs b/src/Umbraco.Core/Logging/WebProfilerComponent.cs index 9d779356f6..238173a935 100644 --- a/src/Umbraco.Core/Logging/WebProfilerComponent.cs +++ b/src/Umbraco.Core/Logging/WebProfilerComponent.cs @@ -19,27 +19,21 @@ namespace Umbraco.Core.Logging public void Initialize(IProfiler profiler, ILogger logger, IRuntimeState runtime) { - // although registered in WebRuntime.Compose, ensure that we have - // not been replaced by another component, and we are still "the" profiler + // although registered in WebRuntime.Compose, ensure that we have not + // been replaced by another component, and we are still "the" profiler _profiler = profiler as WebProfiler; - if (_profiler == null) return; + if (_profiler == null) + { + // if VoidProfiler was registered, let it be known + var vp = profiler as VoidProfiler; + if (vp != null) + logger.Info("Profiler is VoidProfiler, not profiling (must run debug mode to profile)."); + return; + } - if (SystemUtilities.GetCurrentTrustLevel() < AspNetHostingPermissionLevel.High) - { - // if we don't have a high enough trust level we cannot bind to the events - logger.Info("Cannot install when the application is running in Medium trust."); - } - else if (runtime.Debug == false) - { - // only when debugging - logger.Info("Cannot install when the application is not running in debug mode."); - } - else - { - // bind to ApplicationInit - ie execute the application initialization for *each* application - // it would be a mistake to try and bind to the current application events - UmbracoApplicationBase.ApplicationInit += InitializeApplication; - } + // bind to ApplicationInit - ie execute the application initialization for *each* application + // it would be a mistake to try and bind to the current application events + UmbracoApplicationBase.ApplicationInit += InitializeApplication; } private void InitializeApplication(object sender, EventArgs args) diff --git a/src/Umbraco.Core/Logging/WebProfilerProvider.cs b/src/Umbraco.Core/Logging/WebProfilerProvider.cs new file mode 100644 index 0000000000..d2114362e4 --- /dev/null +++ b/src/Umbraco.Core/Logging/WebProfilerProvider.cs @@ -0,0 +1,120 @@ +using System; +using System.Threading; +using System.Web; +using StackExchange.Profiling; + +namespace Umbraco.Core.Logging +{ + /// + /// This is a custom MiniProfiler WebRequestProfilerProvider (which is generally the default) that allows + /// us to profile items during app startup - before an HttpRequest is created + /// + /// + /// Once the boot phase is changed to BootPhase.BootRequest then the base class (default) provider will handle all + /// profiling data and this sub class no longer performs any logic. + /// + internal class WebProfilerProvider : WebRequestProfilerProvider + { + private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim(); + private MiniProfiler _startupProfiler; + private int _first; + private volatile BootPhase _bootPhase; + + public WebProfilerProvider() + { + // booting... + _bootPhase = BootPhase.Boot; + } + + /// + /// Indicates the boot phase. + /// + private enum BootPhase + { + Boot = 0, // boot phase (before the 1st request even begins) + BootRequest = 1, // request boot phase (during the 1st request) + Booted = 2 // done booting + } + + public void BeginBootRequest() + { + _locker.EnterWriteLock(); + try + { + if (_bootPhase != BootPhase.Boot) + throw new InvalidOperationException("Invalid boot phase."); + _bootPhase = BootPhase.BootRequest; + + // assign the profiler to be the current MiniProfiler for the request + // is's already active, starting and all + HttpContext.Current.Items[":mini-profiler:"] = _startupProfiler; + } + finally + { + _locker.ExitWriteLock(); + } + } + + public void EndBootRequest() + { + _locker.EnterWriteLock(); + try + { + if (_bootPhase != BootPhase.BootRequest) + throw new InvalidOperationException("Invalid boot phase."); + _bootPhase = BootPhase.Booted; + + _startupProfiler = null; + } + finally + { + _locker.ExitWriteLock(); + } + } + + /// + /// Starts a new MiniProfiler. + /// + /// + /// This is called when WebProfiler calls MiniProfiler.Start() so, + /// - as a result of WebRuntime starting the WebProfiler, and + /// - assuming profiling is enabled, on every BeginRequest that should be profiled, + /// - except for the very first one which is the boot request. + /// + public override MiniProfiler Start(string sessionName) + { + var first = Interlocked.Exchange(ref _first, 1) == 0; + if (first == false) return base.Start(sessionName); + + _startupProfiler = new MiniProfiler("http://localhost/umbraco-startup") { Name = "StartupProfiler" }; + SetProfilerActive(_startupProfiler); + return _startupProfiler; + } + + /// + /// Gets the current profiler. + /// + /// + /// If the boot phase is not Booted, then this will return the startup profiler (this), otherwise + /// returns the base class + /// + public override MiniProfiler GetCurrentProfiler() + { + // if not booting then just use base (fast) + // no lock, _bootPhase is volatile + if (_bootPhase == BootPhase.Booted) + return base.GetCurrentProfiler(); + + // else + try + { + var current = base.GetCurrentProfiler(); + return current ?? _startupProfiler; + } + catch + { + return _startupProfiler; + } + } + } +} diff --git a/src/Umbraco.Core/MainDom.cs b/src/Umbraco.Core/MainDom.cs index e30f2eecac..87ada14502 100644 --- a/src/Umbraco.Core/MainDom.cs +++ b/src/Umbraco.Core/MainDom.cs @@ -7,8 +7,16 @@ using Umbraco.Core.Logging; namespace Umbraco.Core { - // represents the main domain - class MainDom : IRegisteredObject + /// + /// Represents the main AppDomain running for a given application. + /// + /// + /// There can be only one "main" AppDomain running for a given application at a time. + /// When an AppDomain starts, it tries to acquire the main domain status. + /// When an AppDomain stops (eg the application is restarting) it should release the main domain status. + /// It is possible to register against the MainDom and be notified when it is released. + /// + internal class MainDom : IRegisteredObject { #region Vars @@ -39,7 +47,7 @@ namespace Umbraco.Core #region Ctor // initializes a new instance of MainDom - public MainDom(ILogger logger) + internal MainDom(ILogger logger) { _logger = logger; @@ -48,22 +56,47 @@ namespace Umbraco.Core if (HostingEnvironment.ApplicationID != null) appId = HostingEnvironment.ApplicationID.ReplaceNonAlphanumericChars(string.Empty); - var lockName = "UMBRACO-" + appId + "-MAINDOM-LCK"; + // combining with the physical path because if running on eg IIS Express, + // two sites could have the same appId even though they are different. + // + // now what could still collide is... two sites, running in two different processes + // and having the same appId, and running on the same app physical path + // + // we *cannot* use the process ID here because when an AppPool restarts it is + // a new process for the same application path + + var appPath = HostingEnvironment.ApplicationPhysicalPath; + var hash = (appId + ":::" + appPath).ToSHA1(); + + var lockName = "UMBRACO-" + hash + "-MAINDOM-LCK"; _asyncLock = new AsyncLock(lockName); - var eventName = "UMBRACO-" + appId + "-MAINDOM-EVT"; + var eventName = "UMBRACO-" + hash + "-MAINDOM-EVT"; _signal = new EventWaitHandle(false, EventResetMode.AutoReset, eventName); } #endregion - // register a main domain consumer + /// + /// Registers a resource that requires the current AppDomain to be the main domain to function. + /// + /// An action to execute before the AppDomain releases the main domain status. + /// An optional weight (lower goes first). + /// A value indicating whether it was possible to register. public bool Register(Action release, int weight = 100) { return Register(null, release, weight); } - // register a main domain consumer + /// + /// Registers a resource that requires the current AppDomain to be the main domain to function. + /// + /// An action to execute when registering. + /// An action to execute before the AppDomain releases the main domain status. + /// An optional weight (lower goes first). + /// A value indicating whether it was possible to register. + /// If registering is successful, then the action + /// is guaranteed to execute before the AppDomain releases the main domain status. public bool Register(Action install, Action release, int weight = 100) { lock (_locko) @@ -118,7 +151,7 @@ namespace Umbraco.Core } // acquires the main domain - public bool Acquire() + internal bool Acquire() { lock (_locko) // we don't want the hosting environment to interfere by signaling { @@ -166,7 +199,7 @@ namespace Umbraco.Core public bool IsMainDom => _isMainDom; // IRegisteredObject - public void Stop(bool immediate) + void IRegisteredObject.Stop(bool immediate) { try { diff --git a/src/Umbraco.Core/Media/ImageExtensions.cs b/src/Umbraco.Core/Media/ImageExtensions.cs new file mode 100644 index 0000000000..c20be13d31 --- /dev/null +++ b/src/Umbraco.Core/Media/ImageExtensions.cs @@ -0,0 +1,21 @@ +using System.Drawing; +using System.Drawing.Imaging; +using System.Linq; + +namespace Umbraco.Core.Media +{ + public static class ImageExtensions + { + /// + /// Gets the MIME type of an image. + /// + /// The image. + /// The MIME type of the image. + public static string GetMimeType(this Image image) + { + var format = image.RawFormat; + var codec = ImageCodecInfo.GetImageDecoders().First(c => c.FormatID == format.Guid); + return codec.MimeType; + } + } +} diff --git a/src/Umbraco.Core/Media/ImageHelper.cs b/src/Umbraco.Core/Media/ImageHelper.cs index 99fc278e61..481f8606fa 100644 --- a/src/Umbraco.Core/Media/ImageHelper.cs +++ b/src/Umbraco.Core/Media/ImageHelper.cs @@ -3,259 +3,39 @@ using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; -using System.Globalization; using System.IO; using System.Linq; +using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; using Umbraco.Core.Media.Exif; +using Umbraco.Core.Models; +using Umbraco.Core.Services; namespace Umbraco.Core.Media { /// - /// A helper class used for imaging + /// Provides helper methods for managing images. /// - internal static class ImageHelper + public class ImageHelper // fixme kill! { - /// - /// Gets the dimensions of an image based on a stream - /// - /// - /// - /// - /// First try with EXIF, this is because it is insanely faster and doesn't use any memory to read exif data than to load in the entire - /// image via GDI. Otherwise loading an image into GDI consumes a crazy amount of memory on large images. - /// - /// Of course EXIF data might not exist in every file and can only exist in JPGs - /// - public static Size GetDimensions(Stream imageStream) + private readonly IContentSection _contentSection; + + public ImageHelper(IContentSection contentSection) { - //Try to load with exif - try - { - var jpgInfo = ImageFile.FromStream(imageStream); - - if (jpgInfo.Format != ImageFileFormat.Unknown - && jpgInfo.Properties.ContainsKey(ExifTag.PixelYDimension) - && jpgInfo.Properties.ContainsKey(ExifTag.PixelXDimension)) - { - var height = Convert.ToInt32(jpgInfo.Properties[ExifTag.PixelYDimension].Value); - var width = Convert.ToInt32(jpgInfo.Properties[ExifTag.PixelXDimension].Value); - if (height > 0 && width > 0) - { - return new Size(width, height); - } - } - } - catch (Exception) - { - //We will just swallow, just means we can't read exif data, we don't want to log an error either - } - - //we have no choice but to try to read in via GDI - using (var image = Image.FromStream(imageStream)) - { - - var fileWidth = image.Width; - var fileHeight = image.Height; - return new Size(fileWidth, fileHeight); - } - - } - - public static string GetMimeType(this Image image) - { - var format = image.RawFormat; - var codec = ImageCodecInfo.GetImageDecoders().First(c => c.FormatID == format.Guid); - return codec.MimeType; + _contentSection = contentSection; } /// - /// Creates the thumbnails if the image is larger than all of the specified ones. + /// Gets a value indicating whether the file extension corresponds to an image. /// - /// - /// - /// - /// - /// - /// - internal static IEnumerable GenerateMediaThumbnails( - IFileSystem fs, - string fileName, - string extension, - Image originalImage, - IEnumerable additionalThumbSizes) + /// The file extension. + /// A value indicating whether the file extension corresponds to an image. + public bool IsImageFile(string extension) { - - var result = new List(); - - var allSizesDictionary = new Dictionary { { 100, "thumb" }, { 500, "big-thumb" } }; - - //combine the static dictionary with the additional sizes with only unique values - var allSizes = allSizesDictionary.Select(kv => kv.Key) - .Union(additionalThumbSizes.Where(x => x > 0).Distinct()); - - var sizesDictionary = allSizes.ToDictionary(s => s, s => allSizesDictionary.ContainsKey(s) ? allSizesDictionary[s] : ""); - - foreach (var s in sizesDictionary) - { - var size = s.Key; - var name = s.Value; - if (originalImage.Width >= size && originalImage.Height >= size) - { - result.Add(Resize(fs, fileName, extension, size, name, originalImage)); - } - } - - return result; + if (extension == null) return false; + extension = extension.TrimStart('.'); + return _contentSection.ImageFileTypes.InvariantContains(extension); } - - /// - /// Performs an image resize - /// - /// - /// - /// - /// - /// - /// - /// - private static ResizedImage Resize(IFileSystem fileSystem, string path, string extension, int maxWidthHeight, string fileNameAddition, Image originalImage) - { - var fileNameThumb = string.IsNullOrWhiteSpace(fileNameAddition) - ? string.Format("{0}_UMBRACOSYSTHUMBNAIL." + extension, path.Substring(0, path.LastIndexOf(".", StringComparison.Ordinal))) - : string.Format("{0}_{1}." + extension, path.Substring(0, path.LastIndexOf(".", StringComparison.Ordinal)), fileNameAddition); - - var thumb = GenerateThumbnail( - originalImage, - maxWidthHeight, - fileNameThumb, - extension, - fileSystem); - - return thumb; - } - - internal static ResizedImage GenerateThumbnail(Image image, int maxWidthHeight, string thumbnailFileName, string extension, IFileSystem fs) - { - return GenerateThumbnail(image, maxWidthHeight, -1, -1, thumbnailFileName, extension, fs); - } - - internal static ResizedImage GenerateThumbnail(Image image, int fixedWidth, int fixedHeight, string thumbnailFileName, string extension, IFileSystem fs) - { - return GenerateThumbnail(image, -1, fixedWidth, fixedHeight, thumbnailFileName, extension, fs); - } - - private static ResizedImage GenerateThumbnail(Image image, int maxWidthHeight, int fixedWidth, int fixedHeight, string thumbnailFileName, string extension, IFileSystem fs) - { - // Generate thumbnail - float f = 1; - if (maxWidthHeight >= 0) - { - var fx = (float)image.Size.Width / maxWidthHeight; - var fy = (float)image.Size.Height / maxWidthHeight; - - // must fit in thumbnail size - f = Math.Max(fx, fy); - } - - //depending on if we are doing fixed width resizing or not. - fixedWidth = (maxWidthHeight > 0) ? image.Width : fixedWidth; - fixedHeight = (maxWidthHeight > 0) ? image.Height : fixedHeight; - - var widthTh = (int)Math.Round(fixedWidth / f); - var heightTh = (int)Math.Round(fixedHeight / f); - - // fixes for empty width or height - if (widthTh == 0) - widthTh = 1; - if (heightTh == 0) - heightTh = 1; - - // Create new image with best quality settings - using (var bp = new Bitmap(widthTh, heightTh)) - { - using (var g = Graphics.FromImage(bp)) - { - //if the image size is rather large we cannot use the best quality interpolation mode - // because we'll get out of mem exceptions. So we'll detect how big the image is and use - // the mid quality interpolation mode when the image size exceeds our max limit. - - if (image.Width > 5000 || image.Height > 5000) - { - //use mid quality - g.InterpolationMode = InterpolationMode.Bilinear; - } - else - { - //use best quality - g.InterpolationMode = InterpolationMode.HighQualityBicubic; - } - - - g.SmoothingMode = SmoothingMode.HighQuality; - g.PixelOffsetMode = PixelOffsetMode.HighQuality; - g.CompositingQuality = CompositingQuality.HighQuality; - - // Copy the old image to the new and resized - var rect = new Rectangle(0, 0, widthTh, heightTh); - g.DrawImage(image, rect, 0, 0, image.Width, image.Height, GraphicsUnit.Pixel); - - // Copy metadata - var imageEncoders = ImageCodecInfo.GetImageEncoders(); - ImageCodecInfo codec; - switch (extension.ToLower()) - { - case "png": - codec = imageEncoders.Single(t => t.MimeType.Equals("image/png")); - break; - case "gif": - codec = imageEncoders.Single(t => t.MimeType.Equals("image/gif")); - break; - case "tif": - case "tiff": - codec = imageEncoders.Single(t => t.MimeType.Equals("image/tiff")); - break; - case "bmp": - codec = imageEncoders.Single(t => t.MimeType.Equals("image/bmp")); - break; - // TODO: this is dirty, defaulting to jpg but the return value of this thing is used all over the - // place so left it here, but it needs to not set a codec if it doesn't know which one to pick - // Note: when fixing this: both .jpg and .jpeg should be handled as extensions - default: - codec = imageEncoders.Single(t => t.MimeType.Equals("image/jpeg")); - break; - } - - // Set compresion ratio to 90% - var ep = new EncoderParameters(); - ep.Param[0] = new EncoderParameter(Encoder.Quality, 90L); - - // Save the new image using the dimensions of the image - var predictableThumbnailName = thumbnailFileName.Replace("UMBRACOSYSTHUMBNAIL", maxWidthHeight.ToString(CultureInfo.InvariantCulture)); - var predictableThumbnailNameJpg = predictableThumbnailName.Substring(0, predictableThumbnailName.LastIndexOf(".", StringComparison.Ordinal)) + ".jpg"; - using (var ms = new MemoryStream()) - { - bp.Save(ms, codec, ep); - ms.Seek(0, 0); - - fs.AddFile(predictableThumbnailName, ms); - fs.AddFile(predictableThumbnailNameJpg, ms); - } - - // TODO: Remove this, this is ONLY here for backwards compatibility but it is essentially completely unusable see U4-5385 - var newFileName = thumbnailFileName.Replace("UMBRACOSYSTHUMBNAIL", string.Format("{0}x{1}", widthTh, heightTh)); - using (var ms = new MemoryStream()) - { - bp.Save(ms, codec, ep); - ms.Seek(0, 0); - - fs.AddFile(newFileName, ms); - } - - return new ResizedImage(widthTh, heightTh, newFileName); - } - } - } - } } diff --git a/src/Umbraco.Core/Media/MediaSubfolderCounter.cs b/src/Umbraco.Core/Media/MediaSubfolderCounter.cs deleted file mode 100644 index 6f0142cacf..0000000000 --- a/src/Umbraco.Core/Media/MediaSubfolderCounter.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using Umbraco.Core.IO; - -namespace Umbraco.Core.Media -{ - /// - /// Internal singleton to handle the numbering of subfolders within the Media-folder. - /// When this class is initiated it will look for numbered subfolders and select the highest number, - /// which will be the start point for the naming of the next subfolders. If no subfolders exists - /// then the starting point will be 1000, ie. /media/1000/koala.jpg - /// - internal class MediaSubfolderCounter - { - #region Singleton - - private long _numberedFolder = 1000;//Default starting point - private static readonly ReaderWriterLockSlim ClearLock = new ReaderWriterLockSlim(); - private static readonly Lazy Lazy = new Lazy(() => new MediaSubfolderCounter()); - - public static MediaSubfolderCounter Current { get { return Lazy.Value; } } - - private MediaSubfolderCounter() - { - var folders = new List(); - var fs = FileSystemProviderManager.Current.GetFileSystemProvider(); - var directories = fs.GetDirectories(""); - foreach (var directory in directories) - { - long dirNum; - if (long.TryParse(directory, out dirNum)) - { - folders.Add(dirNum); - } - } - var last = folders.OrderBy(x => x).LastOrDefault(); - if(last != default(long)) - _numberedFolder = last; - } - - #endregion - - /// - /// Returns an increment of the numbered media subfolders. - /// - /// A value - public long Increment() - { - using (new ReadLock(ClearLock)) - { - _numberedFolder = _numberedFolder + 1; - return _numberedFolder; - } - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Media/UploadAutoFillProperties.cs b/src/Umbraco.Core/Media/UploadAutoFillProperties.cs new file mode 100644 index 0000000000..92e1ba05b4 --- /dev/null +++ b/src/Umbraco.Core/Media/UploadAutoFillProperties.cs @@ -0,0 +1,219 @@ +using System; +using System.Drawing; +using System.IO; +using System.Linq; +using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; + +namespace Umbraco.Core.Media +{ + /// + /// Provides methods to manage auto-fill properties for upload fields. + /// + internal class UploadAutoFillProperties + { + private readonly ILogger _logger; + private readonly MediaFileSystem _mediaFileSystem; + private readonly IContentSection _contentSettings; + + public UploadAutoFillProperties(MediaFileSystem mediaFileSystem, ILogger logger, IContentSection contentSettings) + { + _mediaFileSystem = mediaFileSystem; + _logger = logger; + _contentSettings = contentSettings; + } + + /// + /// Gets the auto-fill configuration for a specified property alias. + /// + /// The property type alias. + /// The auto-fill configuration for the specified property alias, or null. + public IImagingAutoFillUploadField GetConfig(string propertyTypeAlias) + { + var autoFillConfigs = _contentSettings.ImageAutoFillProperties; + return autoFillConfigs?.FirstOrDefault(x => x.Alias == propertyTypeAlias); + } + + /// + /// Resets the auto-fill properties of a content item, for a specified property alias. + /// + /// The content item. + /// The property type alias. + public void Reset(IContentBase content, string propertyTypeAlias) + { + if (content == null) throw new ArgumentNullException(nameof(content)); + if (propertyTypeAlias == null) throw new ArgumentNullException(nameof(propertyTypeAlias)); + + // get the config, no config = nothing to do + var autoFillConfig = GetConfig(propertyTypeAlias); + if (autoFillConfig == null) return; // nothing + + // reset + Reset(content, autoFillConfig); + } + + /// + /// Resets the auto-fill properties of a content item, for a specified auto-fill configuration. + /// + /// The content item. + /// The auto-fill configuration. + public void Reset(IContentBase content, IImagingAutoFillUploadField autoFillConfig) + { + if (content == null) throw new ArgumentNullException(nameof(content)); + if (autoFillConfig == null) throw new ArgumentNullException(nameof(autoFillConfig)); + + ResetProperties(content, autoFillConfig); + } + + /// + /// Populates the auto-fill properties of a content item. + /// + /// The content item. + /// The property type alias. + /// The filesystem-relative filepath, or null to clear properties. + public void Populate(IContentBase content, string propertyTypeAlias, string filepath) + { + if (content == null) throw new ArgumentNullException(nameof(content)); + if (propertyTypeAlias == null) throw new ArgumentNullException(nameof(propertyTypeAlias)); + + // no property = nothing to do + if (content.Properties.Contains(propertyTypeAlias) == false) return; + + // get the config, no config = nothing to do + var autoFillConfig = GetConfig(propertyTypeAlias); + if (autoFillConfig == null) return; // nothing + + // populate + Populate(content, autoFillConfig, filepath); + } + + /// + /// Populates the auto-fill properties of a content item. + /// + /// The content item. + /// The property type alias. + /// The filesystem-relative filepath, or null to clear properties. + /// The stream containing the file data. + /// The file data as an image object. + public void Populate(IContentBase content, string propertyTypeAlias, string filepath, Stream filestream, Image image = null) + { + if (content == null) throw new ArgumentNullException(nameof(content)); + if (propertyTypeAlias == null) throw new ArgumentNullException(nameof(propertyTypeAlias)); + + // no property = nothing to do + if (content.Properties.Contains(propertyTypeAlias) == false) return; + + // get the config, no config = nothing to do + var autoFillConfig = GetConfig(propertyTypeAlias); + if (autoFillConfig == null) return; // nothing + + // populate + Populate(content, autoFillConfig, filepath, filestream, image); + } + + /// + /// Populates the auto-fill properties of a content item, for a specified auto-fill configuration. + /// + /// The content item. + /// The auto-fill configuration. + /// The filesystem path to the uploaded file. + /// The parameter is the path relative to the filesystem. + public void Populate(IContentBase content, IImagingAutoFillUploadField autoFillConfig, string filepath) + { + if (content == null) throw new ArgumentNullException(nameof(content)); + if (autoFillConfig == null) throw new ArgumentNullException(nameof(autoFillConfig)); + + // no file = reset, file = auto-fill + if (filepath.IsNullOrWhiteSpace()) + { + ResetProperties(content, autoFillConfig); + } + else + { + // if anything goes wrong, just reset the properties + try + { + using (var filestream = _mediaFileSystem.OpenFile(filepath)) + { + var extension = (Path.GetExtension(filepath) ?? "").TrimStart('.'); + var size = _mediaFileSystem.IsImageFile(extension) ? (Size?)_mediaFileSystem.GetDimensions(filestream) : null; + SetProperties(content, autoFillConfig, size, filestream.Length, extension); + } + } + catch (Exception ex) + { + _logger.Error(typeof(UploadAutoFillProperties), $"Could not populate upload auto-fill properties for file \"{filepath}\".", ex); + ResetProperties(content, autoFillConfig); + } + } + } + + /// + /// Populates the auto-fill properties of a content item. + /// + /// The content item. + /// + /// The filesystem-relative filepath, or null to clear properties. + /// The stream containing the file data. + /// The file data as an image object. + public void Populate(IContentBase content, IImagingAutoFillUploadField autoFillConfig, string filepath, Stream filestream, Image image = null) + { + if (content == null) throw new ArgumentNullException(nameof(content)); + if (autoFillConfig == null) throw new ArgumentNullException(nameof(autoFillConfig)); + + // no file = reset, file = auto-fill + if (filepath.IsNullOrWhiteSpace() || filestream == null) + { + ResetProperties(content, autoFillConfig); + } + else + { + var extension = (Path.GetExtension(filepath) ?? "").TrimStart('.'); + Size? size; + if (image == null) + size = _mediaFileSystem.IsImageFile(extension) ? (Size?) _mediaFileSystem.GetDimensions(filestream) : null; + else + size = new Size(image.Width, image.Height); + SetProperties(content, autoFillConfig, size, filestream.Length, extension); + } + } + + private static void SetProperties(IContentBase content, IImagingAutoFillUploadField autoFillConfig, Size? size, long length, string extension) + { + if (content == null) throw new ArgumentNullException(nameof(content)); + if (autoFillConfig == null) throw new ArgumentNullException(nameof(autoFillConfig)); + + if (content.Properties.Contains(autoFillConfig.WidthFieldAlias)) + content.Properties[autoFillConfig.WidthFieldAlias].Value = size.HasValue ? size.Value.Width.ToInvariantString() : string.Empty; + + if (content.Properties.Contains(autoFillConfig.HeightFieldAlias)) + content.Properties[autoFillConfig.HeightFieldAlias].Value = size.HasValue ? size.Value.Height.ToInvariantString() : string.Empty; + + if (content.Properties.Contains(autoFillConfig.LengthFieldAlias)) + content.Properties[autoFillConfig.LengthFieldAlias].Value = length; + + if (content.Properties.Contains(autoFillConfig.ExtensionFieldAlias)) + content.Properties[autoFillConfig.ExtensionFieldAlias].Value = extension; + } + + private static void ResetProperties(IContentBase content, IImagingAutoFillUploadField autoFillConfig) + { + if (content == null) throw new ArgumentNullException(nameof(content)); + if (autoFillConfig == null) throw new ArgumentNullException(nameof(autoFillConfig)); + + if (content.Properties.Contains(autoFillConfig.WidthFieldAlias)) + content.Properties[autoFillConfig.WidthFieldAlias].Value = string.Empty; + + if (content.Properties.Contains(autoFillConfig.HeightFieldAlias)) + content.Properties[autoFillConfig.HeightFieldAlias].Value = string.Empty; + + if (content.Properties.Contains(autoFillConfig.LengthFieldAlias)) + content.Properties[autoFillConfig.LengthFieldAlias].Value = string.Empty; + + if (content.Properties.Contains(autoFillConfig.ExtensionFieldAlias)) + content.Properties[autoFillConfig.ExtensionFieldAlias].Value = string.Empty; + } + } +} diff --git a/src/Umbraco.Core/Models/ContentBase.cs b/src/Umbraco.Core/Models/ContentBase.cs index 9de5afddb4..b5a6f2ddc2 100644 --- a/src/Umbraco.Core/Models/ContentBase.cs +++ b/src/Umbraco.Core/Models/ContentBase.cs @@ -22,7 +22,7 @@ namespace Umbraco.Core.Models public abstract class ContentBase : Entity, IContentBase { protected IContentTypeComposition ContentTypeBase; - + private Lazy _parentId; private string _name;//NOTE Once localization is introduced this will be the localized Name of the Content/Media. private int _sortOrder; @@ -95,7 +95,7 @@ namespace Umbraco.Core.Models public readonly PropertyInfo TrashedSelector = ExpressionHelper.GetPropertyInfo(x => x.Trashed); public readonly PropertyInfo DefaultContentTypeIdSelector = ExpressionHelper.GetPropertyInfo(x => x.ContentTypeId); public readonly PropertyInfo PropertyCollectionSelector = ExpressionHelper.GetPropertyInfo(x => x.Properties); - } + } protected void PropertiesChanged(object sender, NotifyCollectionChangedEventArgs e) { @@ -183,7 +183,7 @@ namespace Umbraco.Core.Models get { return _creatorId; } set { SetPropertyValueAndDetectChanges(value, ref _creatorId, Ps.Value.CreatorIdSelector); } } - + /// /// Boolean indicating whether this Content is Trashed or not. /// If Content is Trashed it will be located in the Recyclebin. @@ -303,7 +303,7 @@ namespace Umbraco.Core.Models return; } - // .NET magic to call one of the 'SetPropertyValue' handlers with matching signature + // .NET magic to call one of the 'SetPropertyValue' handlers with matching signature ((dynamic)this).SetPropertyValue(propertyTypeAlias, (dynamic)value); } @@ -382,7 +382,7 @@ namespace Umbraco.Core.Models /// Sets the value of a Property /// /// Alias of the PropertyType - /// Value to set for the Property + /// Value to set for the Property public virtual void SetPropertyValue(string propertyTypeAlias, HttpPostedFile value) { ContentExtensions.SetValue(this, propertyTypeAlias, value); @@ -393,14 +393,6 @@ namespace Umbraco.Core.Models /// /// Alias of the PropertyType /// Value to set for the Property - /// - public virtual void SetPropertyValue(string propertyTypeAlias, HttpPostedFileBase value, IDataTypeService dataTypeService) - { - ContentExtensions.SetValue(this, propertyTypeAlias, value, dataTypeService); - } - - [EditorBrowsable(EditorBrowsableState.Never)] - [Obsolete("Use the overload with the IDataTypeService parameter instead")] public virtual void SetPropertyValue(string propertyTypeAlias, HttpPostedFileBase value) { ContentExtensions.SetValue(this, propertyTypeAlias, value); @@ -461,7 +453,7 @@ namespace Umbraco.Core.Models #region Dirty property handling /// - /// We will override this method to ensure that when we reset the dirty properties that we + /// We will override this method to ensure that when we reset the dirty properties that we /// also reset the dirty changes made to the content's Properties (user defined) /// /// @@ -516,8 +508,8 @@ namespace Umbraco.Core.Models /// /// Name of the property to check /// - /// True if any of the class properties are dirty or - /// True if any of the user defined PropertyType properties are dirty based on their alias, + /// True if any of the class properties are dirty or + /// True if any of the user defined PropertyType properties are dirty based on their alias, /// otherwise False /// public override bool IsPropertyDirty(string propertyName) @@ -539,8 +531,8 @@ namespace Umbraco.Core.Models /// /// Name of the property to check /// - /// True if any of the class properties are dirty or - /// True if any of the user defined PropertyType properties are dirty based on their alias, + /// True if any of the class properties are dirty or + /// True if any of the user defined PropertyType properties are dirty based on their alias, /// otherwise False /// public override bool WasPropertyDirty(string propertyName) diff --git a/src/Umbraco.Core/Models/ContentExtensions.cs b/src/Umbraco.Core/Models/ContentExtensions.cs index feff196a41..4df84427cf 100644 --- a/src/Umbraco.Core/Models/ContentExtensions.cs +++ b/src/Umbraco.Core/Models/ContentExtensions.cs @@ -1,19 +1,14 @@ using System; using System.Collections.Generic; using System.ComponentModel; -using System.Drawing; -using System.Globalization; using System.IO; using System.Linq; using System.Web; using System.Xml.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using Umbraco.Core.Configuration; -using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.DI; using Umbraco.Core.IO; -using Umbraco.Core.Media; using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; @@ -22,6 +17,10 @@ namespace Umbraco.Core.Models { public static class ContentExtensions { + // this ain't pretty + private static MediaFileSystem _mediaFileSystem; + private static MediaFileSystem MediaFileSystem => _mediaFileSystem ?? (_mediaFileSystem = Current.FileSystems.MediaFileSystem); + #region IContent /// @@ -383,7 +382,7 @@ namespace Umbraco.Core.Models { if (property.Value is string) { - var value = (string)property.Value; + var value = (string) property.Value; property.Value = value.ToValidXmlString(); } } @@ -498,179 +497,117 @@ namespace Umbraco.Core.Models } } + public static IContentTypeComposition GetContentType(this IContentBase contentBase) + { + if (contentBase == null) throw new ArgumentNullException("contentBase"); + + var content = contentBase as IContent; + if (content != null) return content.ContentType; + var media = contentBase as IMedia; + if (media != null) return media.ContentType; + var member = contentBase as IMember; + if (member != null) return member.ContentType; + throw new NotSupportedException("Unsupported IContentBase implementation: " + contentBase.GetType().FullName + "."); + } + #region SetValue for setting file contents /// - /// Sets and uploads the file from a HttpPostedFileBase object as the property value + /// Stores and sets an uploaded HttpPostedFileBase as a property value. /// - /// to add property value to - /// Alias of the property to save the value on - /// The containing the file that will be uploaded - /// - public static void SetValue(this IContentBase content, string propertyTypeAlias, HttpPostedFileBase value, IDataTypeService dataTypeService) - { - // Ensure we get the filename without the path in IE in intranet mode - // http://stackoverflow.com/questions/382464/httppostedfile-filename-different-from-ie - var fileName = value.FileName; - if (fileName.LastIndexOf(@"\") > 0) - fileName = fileName.Substring(fileName.LastIndexOf(@"\") + 1); - - var name = - IOHelper.SafeFileName( - fileName.Substring(fileName.LastIndexOf(IOHelper.DirSepChar) + 1, - fileName.Length - fileName.LastIndexOf(IOHelper.DirSepChar) - 1) - .ToLower()); - - if (string.IsNullOrEmpty(name) == false) - SetFileOnContent(content, propertyTypeAlias, name, value.InputStream, dataTypeService); - } - - [Obsolete("Use the overload with the IDataTypeService parameter instead")] - [EditorBrowsable(EditorBrowsableState.Never)] + /// A content item. + /// The property alias. + /// The uploaded . public static void SetValue(this IContentBase content, string propertyTypeAlias, HttpPostedFileBase value) { - content.SetValue(propertyTypeAlias, value, Current.Services.DataTypeService); + // ensure we get the filename without the path in IE in intranet mode + // http://stackoverflow.com/questions/382464/httppostedfile-filename-different-from-ie + var filename = value.FileName; + var pos = filename.LastIndexOf(@"\", StringComparison.InvariantCulture); + if (pos > 0) + filename = filename.Substring(pos + 1); + + // strip any directory info + pos = filename.LastIndexOf(IOHelper.DirSepChar); + if (pos > 0) + filename = filename.Substring(pos + 1); + + // get a safe filename - should this be done by MediaHelper? + filename = IOHelper.SafeFileName(filename); + if (string.IsNullOrWhiteSpace(filename)) return; + filename = filename.ToLower(); // fixme - er... why? + + MediaFileSystem.SetUploadFile(content, propertyTypeAlias, filename, value.InputStream); } /// - /// Sets and uploads the file from a HttpPostedFile object as the property value + /// Stores and sets an uploaded HttpPostedFile as a property value. /// - /// to add property value to - /// Alias of the property to save the value on - /// The containing the file that will be uploaded - /// - public static void SetValue(this IContentBase content, string propertyTypeAlias, HttpPostedFile value, IDataTypeService dataTypeService) - { - SetValue(content, propertyTypeAlias, new HttpPostedFileWrapper(value), dataTypeService); - } - - [Obsolete("Use the overload with the IDataTypeService parameter instead")] - [EditorBrowsable(EditorBrowsableState.Never)] + /// A content item. + /// The property alias. + /// The uploaded . public static void SetValue(this IContentBase content, string propertyTypeAlias, HttpPostedFile value) { - SetValue(content, propertyTypeAlias, (HttpPostedFileBase)new HttpPostedFileWrapper(value)); + SetValue(content, propertyTypeAlias, (HttpPostedFileBase) new HttpPostedFileWrapper(value)); } /// - /// Sets and uploads the file from a HttpPostedFileWrapper object as the property value + /// Stores and sets an uploaded HttpPostedFileWrapper as a property value. /// - /// to add property value to - /// Alias of the property to save the value on - /// The containing the file that will be uploaded + /// A content item. + /// The property alias. + /// The uploaded . [Obsolete("There is no reason for this overload since HttpPostedFileWrapper inherits from HttpPostedFileBase")] [EditorBrowsable(EditorBrowsableState.Never)] public static void SetValue(this IContentBase content, string propertyTypeAlias, HttpPostedFileWrapper value) { - SetValue(content, propertyTypeAlias, (HttpPostedFileBase)value); + SetValue(content, propertyTypeAlias, (HttpPostedFileBase) value); } /// - /// Sets and uploads the file from a as the property value + /// Stores and sets a file as a property value. /// - /// to add property value to - /// Alias of the property to save the value on - /// Name of the file - /// to save to disk - /// - public static void SetValue(this IContentBase content, string propertyTypeAlias, string fileName, Stream fileStream, IDataTypeService dataTypeService) + /// A content item. + /// The property alias. + /// The name of the file. + /// A stream containing the file data. + /// This really is for FileUpload fields only, and should be obsoleted. For anything else, + /// you need to store the file by yourself using Store and then figure out + /// how to deal with auto-fill properties (if any) and thumbnails (if any) by yourself. + public static void SetValue(this IContentBase content, string propertyTypeAlias, string filename, Stream filestream) { - var name = IOHelper.SafeFileName(fileName); + if (filename == null || filestream == null) return; - if (string.IsNullOrEmpty(name) == false && fileStream != null) - SetFileOnContent(content, propertyTypeAlias, name, fileStream, dataTypeService); + // get a safe filename - should this be done by MediaHelper? + filename = IOHelper.SafeFileName(filename); + if (string.IsNullOrWhiteSpace(filename)) return; + filename = filename.ToLower(); // fixme - er... why? + + MediaFileSystem.SetUploadFile(content, propertyTypeAlias, filename, filestream); } - [Obsolete("Use the overload with the IDataTypeService parameter instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public static void SetValue(this IContentBase content, string propertyTypeAlias, string fileName, Stream fileStream) + /// + /// Stores a file. + /// + /// A content item. + /// The property alias. + /// The name of the file. + /// A stream containing the file data. + /// The original file path, if any. + /// The path to the file, relative to the media filesystem. + /// + /// Does NOT set the property value, so one should probably store the file and then do + /// something alike: property.Value = MediaHelper.FileSystem.GetUrl(filepath). + /// The original file path is used, in the old media file path scheme, to try and reuse + /// the "folder number" that was assigned to the previous file referenced by the property, + /// if any. + /// + public static string StoreFile(this IContentBase content, string propertyTypeAlias, string filename, Stream filestream, string filepath) { - content.SetValue(propertyTypeAlias, fileName, fileStream, Current.Services.DataTypeService); - } - - private static void SetFileOnContent(IContentBase content, string propertyTypeAlias, string filename, Stream fileStream, IDataTypeService dataTypeService) - { - var property = content.Properties.FirstOrDefault(x => x.Alias == propertyTypeAlias); - if (property == null) - return; - - //TODO: ALl of this naming logic needs to be put into the ImageHelper and then we need to change FileUploadPropertyValueEditor to do the same! - - var numberedFolder = MediaSubfolderCounter.Current.Increment(); - var fileName = UmbracoConfig.For.UmbracoSettings().Content.UploadAllowDirectories - ? Path.Combine(numberedFolder.ToString(CultureInfo.InvariantCulture), filename) - : numberedFolder + "-" + filename; - - var extension = Path.GetExtension(filename).Substring(1).ToLowerInvariant(); - - //the file size is the length of the stream in bytes - var fileSize = fileStream.Length; - - var fs = FileSystemProviderManager.Current.GetFileSystemProvider(); - fs.AddFile(fileName, fileStream); - - //Check if file supports resizing and create thumbnails - var supportsResizing = UmbracoConfig.For.UmbracoSettings().Content.ImageFileTypes.InvariantContains(extension); - - //the config section used to auto-fill properties - IImagingAutoFillUploadField uploadFieldConfigNode = null; - - //Check for auto fill of additional properties - if (UmbracoConfig.For.UmbracoSettings().Content.ImageAutoFillProperties != null) - { - uploadFieldConfigNode = UmbracoConfig.For.UmbracoSettings().Content.ImageAutoFillProperties - .FirstOrDefault(x => x.Alias == propertyTypeAlias); - - } - - if (supportsResizing) - { - //get the original image from the original stream - if (fileStream.CanSeek) fileStream.Seek(0, 0); - using (var originalImage = Image.FromStream(fileStream)) - { - var additionalSizes = new List(); - - //Look up Prevalues for this upload datatype - if it is an upload datatype - get additional configured sizes - if (property.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.UploadFieldAlias) - { - //Get Prevalues by the DataType's Id: property.PropertyType.DataTypeId - var values = dataTypeService.GetPreValuesByDataTypeId(property.PropertyType.DataTypeDefinitionId); - var thumbnailSizes = values.FirstOrDefault(); - //Additional thumbnails configured as prevalues on the DataType - if (thumbnailSizes != null) - { - foreach (var thumb in thumbnailSizes.Split(new[] { ";", "," }, StringSplitOptions.RemoveEmptyEntries)) - { - int thumbSize; - if (thumb != "" && int.TryParse(thumb, out thumbSize)) - { - additionalSizes.Add(thumbSize); - } - } - } - } - - ImageHelper.GenerateMediaThumbnails(fs, fileName, extension, originalImage, additionalSizes); - - //while the image is still open, we'll check if we need to auto-populate the image properties - if (uploadFieldConfigNode != null) - { - content.SetValue(uploadFieldConfigNode.WidthFieldAlias, originalImage.Width.ToString(CultureInfo.InvariantCulture)); - content.SetValue(uploadFieldConfigNode.HeightFieldAlias, originalImage.Height.ToString(CultureInfo.InvariantCulture)); - } - - } - } - - //if auto-fill is true, then fill the remaining, non-image properties - if (uploadFieldConfigNode != null) - { - content.SetValue(uploadFieldConfigNode.LengthFieldAlias, fileSize.ToString(CultureInfo.InvariantCulture)); - content.SetValue(uploadFieldConfigNode.ExtensionFieldAlias, extension); - } - - //Set the value of the property to that of the uploaded file's url - property.Value = fs.GetUrl(fileName); + var propertyType = content.GetContentType() + .CompositionPropertyTypes.FirstOrDefault(x => x.Alias.InvariantEquals(propertyTypeAlias)); + if (propertyType == null) throw new ArgumentException("Invalid property type alias " + propertyTypeAlias + "."); + return MediaFileSystem.StoreFile(content, propertyType, filename, filestream, filepath); } #endregion diff --git a/src/Umbraco.Core/Models/EntityBase/Entity.cs b/src/Umbraco.Core/Models/EntityBase/Entity.cs index 637255a1c8..d605759ed1 100644 --- a/src/Umbraco.Core/Models/EntityBase/Entity.cs +++ b/src/Umbraco.Core/Models/EntityBase/Entity.cs @@ -113,8 +113,10 @@ namespace Umbraco.Core.Models.EntityBase /// internal virtual void AddingEntity() { - CreateDate = DateTime.Now; - UpdateDate = DateTime.Now; + if (IsPropertyDirty("CreateDate") == false || _createDate == default(DateTime)) + CreateDate = DateTime.Now; + if (IsPropertyDirty("UpdateDate") == false || _updateDate == default(DateTime)) + UpdateDate = CreateDate; } /// @@ -122,7 +124,8 @@ namespace Umbraco.Core.Models.EntityBase /// internal virtual void UpdatingEntity() { - UpdateDate = DateTime.Now; + if (IsPropertyDirty("UpdateDate") == false || _updateDate == default(DateTime)) + UpdateDate = DateTime.Now; } /// diff --git a/src/Umbraco.Core/Models/IXsltFile.cs b/src/Umbraco.Core/Models/IXsltFile.cs new file mode 100644 index 0000000000..f6d4418f88 --- /dev/null +++ b/src/Umbraco.Core/Models/IXsltFile.cs @@ -0,0 +1,7 @@ +namespace Umbraco.Core.Models +{ + public interface IXsltFile : IFile + { + + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs index 0d803c26e5..50dc7d06f8 100644 --- a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs +++ b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs @@ -19,7 +19,7 @@ namespace Umbraco.Core.Models.Identity Culture = Configuration.GlobalSettings.DefaultUILanguage; } - public virtual async Task GenerateUserIdentityAsync(BackOfficeUserManager manager) + public virtual async Task GenerateUserIdentityAsync(BackOfficeUserManager manager) { // NOTE the authenticationType must match the umbraco one // defined in CookieAuthenticationOptions.AuthenticationType diff --git a/src/Umbraco.Core/Models/MediaExtensions.cs b/src/Umbraco.Core/Models/MediaExtensions.cs index 1f2e1b62b2..a006548773 100644 --- a/src/Umbraco.Core/Models/MediaExtensions.cs +++ b/src/Umbraco.Core/Models/MediaExtensions.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -9,73 +8,56 @@ using Umbraco.Core.PropertyEditors.ValueConverters; namespace Umbraco.Core.Models { - internal static class MediaExtensions + public static class MediaExtensions { /// - /// Hack: we need to put this in a real place, this is currently just used to render the urls for a media item in the back office + /// Gets the url of a media item. /// - /// public static string GetUrl(this IMedia media, string propertyAlias, ILogger logger) { var propertyType = media.PropertyTypes.FirstOrDefault(x => x.Alias.InvariantEquals(propertyAlias)); - if (propertyType != null) + if (propertyType == null) return string.Empty; + + var val = media.Properties[propertyType]; + if (val == null) return string.Empty; + + var jsonString = val.Value as string; + if (jsonString == null) return string.Empty; + + if (propertyType.PropertyEditorAlias == Constants.PropertyEditors.UploadFieldAlias) + return jsonString; + + if (propertyType.PropertyEditorAlias == Constants.PropertyEditors.ImageCropperAlias) { - var val = media.Properties[propertyType]; - if (val != null) + if (jsonString.DetectIsJson() == false) + return jsonString; + + try { - var jsonString = val.Value as string; - if (jsonString != null) - { - if (propertyType.PropertyEditorAlias == Constants.PropertyEditors.ImageCropperAlias) - { - if (jsonString.DetectIsJson()) - { - try - { - var json = JsonConvert.DeserializeObject(jsonString); - if (json["src"] != null) - { - return json["src"].Value(); - } - } - catch (Exception ex) - { - logger.Error("Could not parse the string " + jsonString + " to a json object", ex); - return string.Empty; - } - } - else - { - return jsonString; - } - } - else if (propertyType.PropertyEditorAlias == Constants.PropertyEditors.UploadFieldAlias) - { - return jsonString; - } - //hrm, without knowing what it is, just adding a string here might not be very nice - } + var json = JsonConvert.DeserializeObject(jsonString); + if (json["src"] != null) + return json["src"].Value(); + } + catch (Exception ex) + { + logger.Error("Could not parse the string " + jsonString + " to a json object", ex); + return string.Empty; } } + + // hrm, without knowing what it is, just adding a string here might not be very nice return string.Empty; } /// - /// Hack: we need to put this in a real place, this is currently just used to render the urls for a media item in the back office + /// Gets the urls of a media item. /// - /// public static string[] GetUrls(this IMedia media, IContentSection contentSection, ILogger logger) { - var links = new List(); - var autoFillProperties = contentSection.ImageAutoFillProperties.ToArray(); - if (autoFillProperties.Any()) - { - links.AddRange( - autoFillProperties - .Select(field => media.GetUrl(field.Alias, logger)) - .Where(link => link.IsNullOrWhiteSpace() == false)); - } - return links.ToArray(); + return contentSection.ImageAutoFillProperties + .Select(field => media.GetUrl(field.Alias, logger)) + .Where(link => string.IsNullOrWhiteSpace(link) == false) + .ToArray(); } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Property.cs b/src/Umbraco.Core/Models/Property.cs index e5471217a5..dff6a712c7 100644 --- a/src/Umbraco.Core/Models/Property.cs +++ b/src/Umbraco.Core/Models/Property.cs @@ -135,7 +135,7 @@ namespace Umbraco.Core.Models private static void ThrowTypeException(object value, Type expected, string alias) { - throw new Exception(string.Format("Value \"{0}\" of type \"{1}\" could not be converted" + throw new InvalidOperationException(string.Format("Value \"{0}\" of type \"{1}\" could not be converted" + " to type \"{2}\" which is expected by property type \"{3}\".", value, value.GetType(), expected, alias)); } diff --git a/src/Umbraco.Core/Models/PublicAccessEntry.cs b/src/Umbraco.Core/Models/PublicAccessEntry.cs index 58e3b69394..4af6f9536a 100644 --- a/src/Umbraco.Core/Models/PublicAccessEntry.cs +++ b/src/Umbraco.Core/Models/PublicAccessEntry.cs @@ -107,9 +107,9 @@ namespace Umbraco.Core.Models public void ClearRules() { - foreach (var rule in _ruleCollection) + for (var i = _ruleCollection.Count - 1; i >= 0; i--) { - RemoveRule(rule); + RemoveRule(_ruleCollection[i]); } } diff --git a/src/Umbraco.Core/Models/Rdbms/RedirectUrlDto.cs b/src/Umbraco.Core/Models/Rdbms/RedirectUrlDto.cs index e709532b91..3733993146 100644 --- a/src/Umbraco.Core/Models/Rdbms/RedirectUrlDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/RedirectUrlDto.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Persistence.DatabaseAnnotations; namespace Umbraco.Core.Models.Rdbms { [TableName("umbracoRedirectUrl")] - [PrimaryKey("id")] + [PrimaryKey("id", AutoIncrement = false)] [ExplicitColumns] class RedirectUrlDto { @@ -22,8 +22,8 @@ namespace Umbraco.Core.Models.Rdbms // inserts, and much faster on reads, so... we have an index on a hash. [Column("id")] - [PrimaryKeyColumn(IdentitySeed = 1, Name = "PK_umbracoRedirectUrl")] - public int Id { get; set; } + [PrimaryKeyColumn(Name = "PK_umbracoRedirectUrl", AutoIncrement = false)] + public Guid Id { get; set; } [ResultColumn] public int ContentId { get; set; } @@ -39,12 +39,12 @@ namespace Umbraco.Core.Models.Rdbms [Column("url")] [NullSetting(NullSetting = NullSettings.NotNull)] - //[Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRedirectUrl", ForColumns = "url, createDateUtc")] public string Url { get; set; } [Column("urlHash")] [NullSetting(NullSetting = NullSettings.NotNull)] [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRedirectUrl", ForColumns = "urlHash, contentKey, createDateUtc")] + [Length(40)] public string UrlHash { get; set; } } } diff --git a/src/Umbraco.Core/Models/Template.cs b/src/Umbraco.Core/Models/Template.cs index e0ceabfd17..5d7f4e552e 100644 --- a/src/Umbraco.Core/Models/Template.cs +++ b/src/Umbraco.Core/Models/Template.cs @@ -15,7 +15,7 @@ using Umbraco.Core.Strings; namespace Umbraco.Core.Models { /// - /// Represents a Template file + /// Represents a Template file. /// [Serializable] [DataContract(IsReference = true)] diff --git a/src/Umbraco.Core/Models/TemplateOnDisk.cs b/src/Umbraco.Core/Models/TemplateOnDisk.cs new file mode 100644 index 0000000000..a8420adcb6 --- /dev/null +++ b/src/Umbraco.Core/Models/TemplateOnDisk.cs @@ -0,0 +1,51 @@ +using System; +using System.Runtime.Serialization; + +namespace Umbraco.Core.Models +{ + /// + /// Represents a Template file that can have its content on disk. + /// + [Serializable] + [DataContract(IsReference = true)] + public class TemplateOnDisk : Template + { + /// + /// Initializes a new instance of the class. + /// + /// The name of the template. + /// The alias of the template. + public TemplateOnDisk(string name, string alias) + : base(name, alias) + { + IsOnDisk = true; + } + + /// + /// Gets or sets a value indicating whether the content is on disk already. + /// + public bool IsOnDisk { get; set; } + + /// + /// Gets or sets the content. + /// + /// + /// Getting the content while the template is "on disk" throws, + /// the template must be saved before its content can be retrieved. + /// Setting the content means it is not "on disk" anymore, and the + /// template becomes (and behaves like) a normal template. + /// + public override string Content + { + get + { + return IsOnDisk ? string.Empty : base.Content; + } + set + { + base.Content = value; + IsOnDisk = false; + } + } + } +} diff --git a/src/Umbraco.Core/Models/UserExtensions.cs b/src/Umbraco.Core/Models/UserExtensions.cs index c444644418..196cfb534a 100644 --- a/src/Umbraco.Core/Models/UserExtensions.cs +++ b/src/Umbraco.Core/Models/UserExtensions.cs @@ -1,8 +1,6 @@ using System; using System.Globalization; using System.Linq; -using System.Threading; -using Umbraco.Core.Models.Identity; using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; @@ -18,6 +16,7 @@ namespace Umbraco.Core.Models /// public static bool IsAdmin(this IUser user) { + if (user == null) throw new ArgumentNullException(nameof(user)); return user.UserType.Alias == "admin"; } @@ -38,8 +37,8 @@ namespace Umbraco.Core.Models /// public static CultureInfo GetUserCulture(this IUser user, ILocalizedTextService textService) { - if (user == null) throw new ArgumentNullException("user"); - if (textService == null) throw new ArgumentNullException("textService"); + if (user == null) throw new ArgumentNullException(nameof(user)); + if (textService == null) throw new ArgumentNullException(nameof(textService)); return GetUserCulture(user.Language, textService); } @@ -69,8 +68,8 @@ namespace Umbraco.Core.Models /// internal static bool HasPathAccess(this IUser user, IContent content) { - if (user == null) throw new ArgumentNullException("user"); - if (content == null) throw new ArgumentNullException("content"); + if (user == null) throw new ArgumentNullException(nameof(user)); + if (content == null) throw new ArgumentNullException(nameof(content)); return HasPathAccess(content.Path, user.StartContentId, Constants.System.RecycleBinContent); } @@ -99,9 +98,9 @@ namespace Umbraco.Core.Models /// internal static bool HasPathAccess(this IUser user, IMedia media) { - if (user == null) throw new ArgumentNullException("user"); - if (media == null) throw new ArgumentNullException("media"); + if (user == null) throw new ArgumentNullException(nameof(user)); + if (media == null) throw new ArgumentNullException(nameof(media)); return HasPathAccess(media.Path, user.StartMediaId, Constants.System.RecycleBinMedia); } - } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/XsltFile.cs b/src/Umbraco.Core/Models/XsltFile.cs new file mode 100644 index 0000000000..07f4d00084 --- /dev/null +++ b/src/Umbraco.Core/Models/XsltFile.cs @@ -0,0 +1,32 @@ +using System; +using System.Runtime.Serialization; + +namespace Umbraco.Core.Models +{ + /// + /// Represents a XSLT file + /// + [Serializable] + [DataContract(IsReference = true)] + public class XsltFile : File, IXsltFile + { + public XsltFile(string path) + : this(path, (Func) null) + { } + + internal XsltFile(string path, Func getFileContent) + : base(path, getFileContent) + { } + + /// + /// Indicates whether the current entity has an identity, which in this case is a path/name. + /// + /// + /// Overrides the default Entity identity check. + /// + public override bool HasIdentity + { + get { return string.IsNullOrEmpty(Path) == false; } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Packaging/PackageBinaryInspector.cs b/src/Umbraco.Core/Packaging/PackageBinaryInspector.cs index 110f6d2420..5847733639 100644 --- a/src/Umbraco.Core/Packaging/PackageBinaryInspector.cs +++ b/src/Umbraco.Core/Packaging/PackageBinaryInspector.cs @@ -288,12 +288,8 @@ namespace Umbraco.Core.Packaging PrivateBinPathProbe = AppDomain.CurrentDomain.SetupInformation.PrivateBinPathProbe }; - //create new domain with full trust - return AppDomain.CreateDomain( - appName, - AppDomain.CurrentDomain.Evidence, - domainSetup, - new PermissionSet(PermissionState.Unrestricted)); + // create new domain with full trust + return AppDomain.CreateDomain(appName, AppDomain.CurrentDomain.Evidence, domainSetup, new PermissionSet(PermissionState.Unrestricted)); } } } diff --git a/src/Umbraco.Core/Persistence/DefaultDatabaseFactory.cs b/src/Umbraco.Core/Persistence/DefaultDatabaseFactory.cs index db469957a9..df45d87e75 100644 --- a/src/Umbraco.Core/Persistence/DefaultDatabaseFactory.cs +++ b/src/Umbraco.Core/Persistence/DefaultDatabaseFactory.cs @@ -262,10 +262,8 @@ namespace Umbraco.Core.Persistence protected override void DisposeResources() { - // this is weird, because _nonHttpInstance is thread-static, so we would need - // to dispose the factory in each thread where a database has been used - else - // it only disposes the current thread's database instance. - // + // this is weird, because hybrid accessors store different databases per + // thread, so we don't really know what we are disposing here... // besides, we don't really want to dispose the factory, which is a singleton... var db = _umbracoDatabaseAccessor.UmbracoDatabase; diff --git a/src/Umbraco.Core/Persistence/Factories/ContentTypeFactory.cs b/src/Umbraco.Core/Persistence/Factories/ContentTypeFactory.cs index 6fa13654ad..3672f80873 100644 --- a/src/Umbraco.Core/Persistence/Factories/ContentTypeFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/ContentTypeFactory.cs @@ -122,7 +122,7 @@ namespace Umbraco.Core.Persistence.Factories else if (entity is IMemberType) nodeObjectType = Constants.ObjectTypes.MemberTypeGuid; else - throw new Exception("oops: invalid entity."); + throw new Exception("Invalid entity."); var contentTypeDto = new ContentTypeDto { diff --git a/src/Umbraco.Core/Persistence/Factories/MemberTypeReadOnlyFactory.cs b/src/Umbraco.Core/Persistence/Factories/MemberTypeReadOnlyFactory.cs index 5e34fb8936..b4d5d2e566 100644 --- a/src/Umbraco.Core/Persistence/Factories/MemberTypeReadOnlyFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/MemberTypeReadOnlyFactory.cs @@ -79,7 +79,7 @@ namespace Umbraco.Core.Persistence.Factories { // note: no idea why Id is nullable here, but better check if (groupDto.Id.HasValue == false) - throw new Exception("oops: groupDto.Id has no value."); + throw new Exception("GroupDto.Id has no value."); group.Id = groupDto.Id.Value; } diff --git a/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs b/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs index 8f2bab7dcb..26a14d2477 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs @@ -283,9 +283,9 @@ namespace Umbraco.Core.Persistence.Migrations.Initial //defaults for the member list _database.Insert("cmsDataTypePreValues", "id", false, new DataTypePreValueDto { Id = -1, Alias = "pageSize", SortOrder = 1, DataTypeNodeId = Constants.DataTypes.DefaultMembersListView, Value = "10" }); - _database.Insert("cmsDataTypePreValues", "id", false, new DataTypePreValueDto { Id = -2, Alias = "orderBy", SortOrder = 2, DataTypeNodeId = Constants.DataTypes.DefaultMembersListView, Value = "Name" }); + _database.Insert("cmsDataTypePreValues", "id", false, new DataTypePreValueDto { Id = -2, Alias = "orderBy", SortOrder = 2, DataTypeNodeId = Constants.DataTypes.DefaultMembersListView, Value = "username" }); _database.Insert("cmsDataTypePreValues", "id", false, new DataTypePreValueDto { Id = -3, Alias = "orderDirection", SortOrder = 3, DataTypeNodeId = Constants.DataTypes.DefaultMembersListView, Value = "asc" }); - _database.Insert("cmsDataTypePreValues", "id", false, new DataTypePreValueDto { Id = -4, Alias = "includeProperties", SortOrder = 4, DataTypeNodeId = Constants.DataTypes.DefaultMembersListView, Value = "[{\"alias\":\"email\",\"isSystem\":1},{\"alias\":\"username\",\"isSystem\":1},{\"alias\":\"updateDate\",\"header\":\"Last edited\",\"isSystem\":1}]" }); + _database.Insert("cmsDataTypePreValues", "id", false, new DataTypePreValueDto { Id = -4, Alias = "includeProperties", SortOrder = 4, DataTypeNodeId = Constants.DataTypes.DefaultMembersListView, Value = "[{\"alias\":\"username\",\"isSystem\":1},{\"alias\":\"email\",\"isSystem\":1},{\"alias\":\"updateDate\",\"header\":\"Last edited\",\"isSystem\":1}]" }); //layouts for the list view var cardLayout = "{\"name\": \"Grid\",\"path\": \"views/propertyeditors/listview/layouts/grid/grid.html\", \"icon\": \"icon-thumbnails-small\", \"isSystem\": 1, \"selected\": true}"; @@ -293,10 +293,10 @@ namespace Umbraco.Core.Persistence.Migrations.Initial //defaults for the media list _database.Insert("cmsDataTypePreValues", "id", false, new DataTypePreValueDto { Id = -5, Alias = "pageSize", SortOrder = 1, DataTypeNodeId = Constants.DataTypes.DefaultMediaListView, Value = "100" }); - _database.Insert("cmsDataTypePreValues", "id", false, new DataTypePreValueDto { Id = -6, Alias = "orderBy", SortOrder = 2, DataTypeNodeId = Constants.DataTypes.DefaultMediaListView, Value = "VersionDate" }); + _database.Insert("cmsDataTypePreValues", "id", false, new DataTypePreValueDto { Id = -6, Alias = "orderBy", SortOrder = 2, DataTypeNodeId = Constants.DataTypes.DefaultMediaListView, Value = "updateDate" }); _database.Insert("cmsDataTypePreValues", "id", false, new DataTypePreValueDto { Id = -7, Alias = "orderDirection", SortOrder = 3, DataTypeNodeId = Constants.DataTypes.DefaultMediaListView, Value = "desc" }); _database.Insert("cmsDataTypePreValues", "id", false, new DataTypePreValueDto { Id = -8, Alias = "layouts", SortOrder = 4, DataTypeNodeId = Constants.DataTypes.DefaultMediaListView, Value = "[" + cardLayout + "," + listLayout + "]" }); - _database.Insert("cmsDataTypePreValues", "id", false, new DataTypePreValueDto { Id = -9, Alias = "includeProperties", SortOrder = 5, DataTypeNodeId = Constants.DataTypes.DefaultMediaListView, Value = "[{\"alias\":\"sortOrder\",\"isSystem\":1, \"header\": \"Sort order\"},{\"alias\":\"updateDate\",\"header\":\"Last edited\",\"isSystem\":1},{\"alias\":\"owner\",\"header\":\"Updated by\",\"isSystem\":1}]" }); + _database.Insert("cmsDataTypePreValues", "id", false, new DataTypePreValueDto { Id = -9, Alias = "includeProperties", SortOrder = 5, DataTypeNodeId = Constants.DataTypes.DefaultMediaListView, Value = "[{\"alias\":\"updateDate\",\"header\":\"Last edited\",\"isSystem\":1},{\"alias\":\"owner\",\"header\":\"Updated by\",\"isSystem\":1}]" }); } private void CreateUmbracoRelationTypeData() diff --git a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaResult.cs b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaResult.cs index 70ab65b422..5b980b6c23 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaResult.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaResult.cs @@ -46,8 +46,13 @@ namespace Umbraco.Core.Persistence.Migrations.Initial /// public SemVersion DetermineInstalledVersionByMigrations(IMigrationEntryService migrationEntryService) { - var allMigrations = migrationEntryService.GetAll(GlobalSettings.UmbracoMigrationName); - var mostrecent = allMigrations.OrderByDescending(x => x.Version).Select(x => x.Version).FirstOrDefault(); + SemVersion mostrecent = null; + + if (ValidTables.Any(x => x.InvariantEquals("umbracoMigration"))) + { + var allMigrations = migrationEntryService.GetAll(GlobalSettings.UmbracoMigrationName); + mostrecent = allMigrations.OrderByDescending(x => x.Version).Select(x => x.Version).FirstOrDefault(); + } return mostrecent ?? new SemVersion(new Version(0, 0, 0)); } @@ -119,13 +124,19 @@ namespace Umbraco.Core.Persistence.Migrations.Initial //if the error is for umbracoAccess it must be the previous version to 7.3 since that is when it is added if (Errors.Any(x => x.Item1.Equals("Table") && (x.Item2.InvariantEquals("umbracoAccess")))) { - return new Version(7, 2, 5); + return new Version(7, 2, 0); } //if the error is for umbracoDeployChecksum it must be the previous version to 7.4 since that is when it is added if (Errors.Any(x => x.Item1.Equals("Table") && (x.Item2.InvariantEquals("umbracoDeployChecksum")))) { - return new Version(7, 3, 4); + return new Version(7, 3, 0); + } + + //if the error is for umbracoRedirectUrl it must be the previous version to 7.5 since that is when it is added + if (Errors.Any(x => x.Item1.Equals("Table") && (x.Item2.InvariantEquals("umbracoRedirectUrl")))) + { + return new Version(7, 4, 0); } return UmbracoVersion.Current; diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionEight/AddRedirectUrlTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionEight/AddRedirectUrlTable.cs index fcae11bfa5..af264c5503 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionEight/AddRedirectUrlTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionEight/AddRedirectUrlTable.cs @@ -18,24 +18,39 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionEight private string MigrationCode(UmbracoDatabase database) { - // don't execute if the table is already there - var tables = SqlSyntax.GetTablesInSchema(database).ToArray(); - if (tables.InvariantContains("umbracoRedirectUrl")) return null; - + var umbracoRedirectUrlTableName = "umbracoRedirectUrl"; var localContext = new LocalMigrationContext(database, Logger); - localContext.Create.Table("umbracoRedirectUrl") - .WithColumn("id").AsInt32().Identity().PrimaryKey("PK_umbracoRedirectUrl") - .WithColumn("contentId").AsInt32().NotNullable() - .WithColumn("createDateUtc").AsDateTime().NotNullable() - .WithColumn("url").AsString(2048).NotNullable(); + var tables = SqlSyntax.GetTablesInSchema(database).ToArray(); - localContext.Create.Index("IX_umbracoRedirectUrl") - .OnTable("umbracoRedirectUrl") - .OnColumn("url").Ascending() - .OnColumn("createDateUtc").Ascending() + if (tables.InvariantContains(umbracoRedirectUrlTableName)) + { + var columns = SqlSyntax.GetColumnsInSchema(database).ToArray(); + if (columns.Any(x => x.TableName.InvariantEquals(umbracoRedirectUrlTableName) && x.ColumnName.InvariantEquals("id") && x.DataType == "uniqueidentifier")) + return null; + localContext.Delete.Table(umbracoRedirectUrlTableName); + } + + localContext.Create.Table(umbracoRedirectUrlTableName) + .WithColumn("id").AsGuid().NotNullable().PrimaryKey("PK_" + umbracoRedirectUrlTableName) + .WithColumn("createDateUtc").AsDateTime().NotNullable() + .WithColumn("url").AsString(2048).NotNullable() + .WithColumn("contentKey").AsGuid().NotNullable() + .WithColumn("urlHash").AsString(40).NotNullable(); + + localContext.Create.Index("IX_" + umbracoRedirectUrlTableName).OnTable(umbracoRedirectUrlTableName) + .OnColumn("urlHash") + .Ascending() + .OnColumn("contentKey") + .Ascending() + .OnColumn("createDateUtc") + .Descending() .WithOptions().NonClustered(); + localContext.Create.ForeignKey("FK_" + umbracoRedirectUrlTableName) + .FromTable(umbracoRedirectUrlTableName).ForeignColumn("contentKey") + .ToTable("umbracoNode").PrimaryColumn("uniqueID"); + return localContext.GetSql(); } diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveZero/RemoveStylesheetDataAndTablesAgain.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveZero/RemoveStylesheetDataAndTablesAgain.cs index e8ea34995b..3b855fbc25 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveZero/RemoveStylesheetDataAndTablesAgain.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveZero/RemoveStylesheetDataAndTablesAgain.cs @@ -19,22 +19,32 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenFiveZer public override void Up() { + // defer, because we are making decisions based upon what's in the database + Execute.Code(MigrationCode); + } + + private string MigrationCode(UmbracoDatabase database) + { + var localContext = new LocalMigrationContext(database, Logger); + //Clear all stylesheet data if the tables exist var tables = SqlSyntax.GetTablesInSchema(Context.Database).ToArray(); if (tables.InvariantContains("cmsStylesheetProperty")) { - Delete.FromTable("cmsStylesheetProperty").AllRows(); - Delete.FromTable("umbracoNode").Row(new { nodeObjectType = new Guid(Constants.ObjectTypes.StylesheetProperty) }); + localContext.Delete.FromTable("cmsStylesheetProperty").AllRows(); + localContext.Delete.FromTable("umbracoNode").Row(new { nodeObjectType = new Guid(Constants.ObjectTypes.StylesheetProperty) }); - Delete.Table("cmsStylesheetProperty"); + localContext.Delete.Table("cmsStylesheetProperty"); } if (tables.InvariantContains("cmsStylesheet")) { - Delete.FromTable("cmsStylesheet").AllRows(); - Delete.FromTable("umbracoNode").Row(new { nodeObjectType = new Guid(Constants.ObjectTypes.Stylesheet) }); + localContext.Delete.FromTable("cmsStylesheet").AllRows(); + localContext.Delete.FromTable("umbracoNode").Row(new { nodeObjectType = new Guid(Constants.ObjectTypes.Stylesheet) }); - Delete.Table("cmsStylesheet"); + localContext.Delete.Table("cmsStylesheet"); } + + return localContext.GetSql(); } public override void Down() diff --git a/src/Umbraco.Core/Persistence/NPocoSqlExtensions.cs b/src/Umbraco.Core/Persistence/NPocoSqlExtensions.cs index f5d7f58330..d16cf7fe0f 100644 --- a/src/Umbraco.Core/Persistence/NPocoSqlExtensions.cs +++ b/src/Umbraco.Core/Persistence/NPocoSqlExtensions.cs @@ -3,8 +3,10 @@ using System.Collections; using System.Linq; using System.Linq.Expressions; using System.Reflection; +using System.Text; using NPoco; using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence { @@ -26,9 +28,24 @@ namespace Umbraco.Core.Persistence public static Sql WhereIn(this Sql sql, Expression> fieldSelector, IEnumerable values) { - var expresionist = new PocoToSqlExpressionHelper(sql.SqlContext); - var fieldExpression = expresionist.Visit(fieldSelector); - sql.Where(fieldExpression + " IN (@values)", new { /*@values =*/ values }); + var fieldName = GetFieldName(fieldSelector, sql.SqlContext.SqlSyntax); + sql.Where(fieldName + " IN (@values)", new { values }); + return sql; + } + + public static Sql WhereAnyIn(this Sql sql, Expression>[] fieldSelectors, IEnumerable values) + { + var fieldNames = fieldSelectors.Select(x => GetFieldName(x, sql.SqlContext.SqlSyntax)).ToArray(); + var sb = new StringBuilder(); + sb.Append("("); + for (var i = 0; i < fieldNames.Length; i++) + { + if (i > 0) sb.Append(" OR "); + sb.Append(fieldNames[i]); + sql.Append(" IN (@values)"); + } + sb.Append(")"); + sql.Where(sb.ToString(), new { values }); return sql; } @@ -39,8 +56,7 @@ namespace Umbraco.Core.Persistence public static Sql From(this Sql sql) { var type = typeof (T); - var tableNameAttribute = type.FirstAttribute(); - var tableName = tableNameAttribute == null ? string.Empty : tableNameAttribute.Value; + var tableName = type.GetTableName(); sql.From(sql.SqlContext.SqlSyntax.GetQuotedTableName(tableName)); return sql; @@ -52,33 +68,14 @@ namespace Umbraco.Core.Persistence public static Sql OrderBy(this Sql sql, Expression> columnMember) { - var column = ExpressionHelper.FindProperty(columnMember) as PropertyInfo; - var columnName = column.FirstAttribute().Name; - - var type = typeof (T); - var tableNameAttribute = type.FirstAttribute(); - var tableName = tableNameAttribute == null ? string.Empty : tableNameAttribute.Value; - - // need to ensure the order by is in brackets, see: https://github.com/toptensoftware/PetaPoco/issues/177 - var sqlSyntax = sql.SqlContext.SqlSyntax; - var syntax = $"({sqlSyntax.GetQuotedTableName(tableName)}.{sqlSyntax.GetQuotedColumnName(columnName)})"; - + var syntax = "(" + GetFieldName(columnMember, sql.SqlContext.SqlSyntax) + ")"; sql.OrderBy(syntax); return sql; } public static Sql OrderByDescending(this Sql sql, Expression> columnMember) { - var column = ExpressionHelper.FindProperty(columnMember) as PropertyInfo; - var columnName = column.FirstAttribute().Name; - - var type = typeof(T); - var tableNameAttribute = type.FirstAttribute(); - var tableName = tableNameAttribute == null ? string.Empty : tableNameAttribute.Value; - - var sqlSyntax = sql.SqlContext.SqlSyntax; - var syntax = $"{sqlSyntax.GetQuotedTableName(tableName)}.{sqlSyntax.GetQuotedColumnName(columnName)} DESC"; - + var syntax = "(" + GetFieldName(columnMember, sql.SqlContext.SqlSyntax) + ") DESC"; sql.OrderBy(syntax); return sql; } @@ -92,7 +89,7 @@ namespace Umbraco.Core.Persistence public static Sql GroupBy(this Sql sql, Expression> columnMember) { var column = ExpressionHelper.FindProperty(columnMember) as PropertyInfo; - var columnName = column.FirstAttribute().Name; + var columnName = column.GetColumnName(); sql.GroupBy(sql.SqlContext.SqlSyntax.GetQuotedColumnName(columnName)); return sql; @@ -105,8 +102,7 @@ namespace Umbraco.Core.Persistence public static Sql.SqlJoinClause InnerJoin(this Sql sql) { var type = typeof(T); - var tableNameAttribute = type.FirstAttribute(); - var tableName = tableNameAttribute == null ? string.Empty : tableNameAttribute.Value; + var tableName = type.GetTableName(); return sql.InnerJoin(sql.SqlContext.SqlSyntax.GetQuotedTableName(tableName)); } @@ -114,8 +110,7 @@ namespace Umbraco.Core.Persistence public static Sql.SqlJoinClause LeftJoin(this Sql sql) { var type = typeof(T); - var tableNameAttribute = type.FirstAttribute(); - var tableName = tableNameAttribute == null ? string.Empty : tableNameAttribute.Value; + var tableName = type.GetTableName(); return sql.LeftJoin(sql.SqlContext.SqlSyntax.GetQuotedTableName(tableName)); } @@ -123,8 +118,7 @@ namespace Umbraco.Core.Persistence public static Sql.SqlJoinClause RightJoin(this Sql sql) { var type = typeof(T); - var tableNameAttribute = type.FirstAttribute(); - var tableName = tableNameAttribute == null ? string.Empty : tableNameAttribute.Value; + var tableName = type.GetTableName(); return sql.RightJoin(sql.SqlContext.SqlSyntax.GetQuotedTableName(tableName)); } @@ -137,15 +131,16 @@ namespace Umbraco.Core.Persistence var leftType = typeof (TLeft); var rightType = typeof (TRight); - var leftTableName = sqlSyntax.GetQuotedTableName(leftType.FirstAttribute().Value); - var rightTableName = sqlSyntax.GetQuotedTableName(rightType.FirstAttribute().Value); + var leftTableName = leftType.GetTableName(); + var rightTableName = rightType.GetTableName(); - var left = ExpressionHelper.FindProperty(leftMember) as PropertyInfo; - var right = ExpressionHelper.FindProperty(rightMember) as PropertyInfo; - var leftColumnName = sqlSyntax.GetQuotedColumnName(left.FirstAttribute().Name); - var rightColumnName = sqlSyntax.GetQuotedColumnName(right.FirstAttribute().Name); + var leftColumn = ExpressionHelper.FindProperty(leftMember) as PropertyInfo; + var rightColumn = ExpressionHelper.FindProperty(rightMember) as PropertyInfo; - var onClause = $"{leftTableName}.{leftColumnName} = {rightTableName}.{rightColumnName}"; + var leftColumnName = leftColumn.GetColumnName(); + var rightColumnName = rightColumn.GetColumnName(); + + string onClause = $"{sqlSyntax.GetQuotedTableName(leftTableName)}.{sqlSyntax.GetQuotedColumnName(leftColumnName)} = {sqlSyntax.GetQuotedTableName(rightTableName)}.{sqlSyntax.GetQuotedColumnName(rightColumnName)}"; return clause.On(onClause); } @@ -153,6 +148,11 @@ namespace Umbraco.Core.Persistence #region Select + public static Sql SelectTop(this Sql sql, int count) + { + return sql.SqlContext.SqlSyntax.SelectTop(sql, count); + } + public static Sql SelectCount(this Sql sql) { sql.Select("COUNT(*)"); @@ -237,5 +237,35 @@ namespace Umbraco.Core.Persistence } #endregion + + #region Helpers + + private static string GetTableName(this Type type) + { + // todo: returning string.Empty for now + // BUT the code bits that calls this method cannot deal with string.Empty so we + // should either throw, or fix these code bits... + var attr = type.FirstAttribute(); + return string.IsNullOrWhiteSpace(attr?.Value) ? string.Empty : attr.Value; + } + + private static string GetColumnName(this PropertyInfo column) + { + var attr = column.FirstAttribute(); + return string.IsNullOrWhiteSpace(attr?.Name) ? column.Name : attr.Name; + } + + private static string GetFieldName(Expression> fieldSelector, ISqlSyntaxProvider sqlSyntax) + { + var field = ExpressionHelper.FindProperty(fieldSelector) as PropertyInfo; + var fieldName = field.GetColumnName(); + + var type = typeof(T); + var tableName = type.GetTableName(); + + return sqlSyntax.GetQuotedTableName(tableName) + "." + sqlSyntax.GetQuotedColumnName(fieldName); + } + + #endregion } } diff --git a/src/Umbraco.Core/Persistence/Querying/BaseExpressionHelper.cs b/src/Umbraco.Core/Persistence/Querying/BaseExpressionHelper.cs index cdd6c9cf24..560e6f6eeb 100644 --- a/src/Umbraco.Core/Persistence/Querying/BaseExpressionHelper.cs +++ b/src/Umbraco.Core/Persistence/Querying/BaseExpressionHelper.cs @@ -448,6 +448,55 @@ namespace Umbraco.Core.Persistence.Querying } return HandleStringComparison(visitedObjectForMethod, compareValue, m.Method.Name, colType); + + case "Replace": + string searchValue; + + if (methodArgs[0].NodeType != ExpressionType.Constant) + { + //This occurs when we are getting a value from a non constant such as: x => x.Path.StartsWith(content.Path) + // So we'll go get the value: + var member = Expression.Convert(methodArgs[0], typeof(object)); + var lambda = Expression.Lambda>(member); + var getter = lambda.Compile(); + searchValue = getter().ToString(); + } + else + { + searchValue = methodArgs[0].ToString(); + } + + if (methodArgs[0].Type != typeof(string) && TypeHelper.IsTypeAssignableFrom(methodArgs[0].Type)) + { + throw new NotSupportedException("An array Contains method is not supported"); + } + + string replaceValue; + + if (methodArgs[1].NodeType != ExpressionType.Constant) + { + //This occurs when we are getting a value from a non constant such as: x => x.Path.StartsWith(content.Path) + // So we'll go get the value: + var member = Expression.Convert(methodArgs[1], typeof(object)); + var lambda = Expression.Lambda>(member); + var getter = lambda.Compile(); + replaceValue = getter().ToString(); + } + else + { + replaceValue = methodArgs[1].ToString(); + } + + if (methodArgs[1].Type != typeof(string) && TypeHelper.IsTypeAssignableFrom(methodArgs[1].Type)) + { + throw new NotSupportedException("An array Contains method is not supported"); + } + + SqlParameters.Add(RemoveQuote(searchValue)); + + SqlParameters.Add(RemoveQuote(replaceValue)); + + return string.Format("replace({0}, @{1}, @{2})", visitedObjectForMethod, SqlParameters.Count - 2, SqlParameters.Count - 1); //case "Substring": // var startIndex = Int32.Parse(args[0].ToString()) + 1; // if (args.Count == 2) diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index 55299c3914..f472520f31 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Xml; using NPoco; using Umbraco.Core.Logging; using Umbraco.Core.Models; @@ -69,7 +70,7 @@ namespace Umbraco.Core.Persistence.Repositories .Where(x => x.Newest) .OrderByDescending(x => x.VersionDate); - var dto = Database.Fetch(sql).FirstOrDefault(); + var dto = Database.Fetch(sql.SelectTop(1)).FirstOrDefault(); if (dto == null) return null; @@ -401,7 +402,8 @@ namespace Umbraco.Core.Persistence.Repositories } else { - entity.UpdateDate = DateTime.Now; + if (entity.IsPropertyDirty("UpdateDate") == false || entity.UpdateDate == default(DateTime)) + entity.UpdateDate = DateTime.Now; } //Ensure unique name on the same level @@ -699,7 +701,7 @@ namespace Umbraco.Core.Persistence.Repositories return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, MapQueryDtos, - orderBy, orderDirection, orderBySystemField, + orderBy, orderDirection, orderBySystemField, "cmsDocument", filterSql); } diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepositoryBase.cs index 34134ae3c6..5a8d9753c4 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepositoryBase.cs @@ -315,8 +315,7 @@ AND umbracoNode.id <> @id", } //Delete the allowed content type entries before adding the updated collection - Database.Delete("WHERE Id = @Id", new { Id = entity.Id }); - //Insert collection of allowed content types + Database.Delete("WHERE Id = @Id", new { entity.Id }); foreach (var allowedContentType in entity.AllowedContentTypes) { Database.Insert(new ContentTypeAllowedContentTypeDto @@ -327,24 +326,23 @@ AND umbracoNode.id <> @id", }); } + // fixme below, manage the property type - if (((ICanBeDirty)entity).IsPropertyDirty("PropertyTypes") || entity.PropertyTypes.Any(x => x.IsDirty())) + // delete ??? fixme wtf is this? + // ... by excepting entries from db with entries from collections + if (entity.IsPropertyDirty("PropertyTypes") || entity.PropertyTypes.Any(x => x.IsDirty())) { - //Delete PropertyTypes by excepting entries from db with entries from collections - var dbPropertyTypes = Database.Fetch("WHERE contentTypeId = @Id", new { Id = entity.Id }); + var dbPropertyTypes = Database.Fetch("WHERE contentTypeId = @Id", new { entity.Id }); var dbPropertyTypeAlias = dbPropertyTypes.Select(x => x.Id); var entityPropertyTypes = entity.PropertyTypes.Where(x => x.HasIdentity).Select(x => x.Id); var items = dbPropertyTypeAlias.Except(entityPropertyTypes); foreach (var item in items) - { - //Before a PropertyType can be deleted, all Properties based on that PropertyType should be deleted. - Database.Delete("WHERE propertyTypeId = @Id", new { Id = item }); - Database.Delete("WHERE propertytypeid = @Id", new { Id = item }); - Database.Delete("WHERE contentTypeId = @Id AND id = @PropertyTypeId", - new { Id = entity.Id, PropertyTypeId = item }); - } + DeletePropertyType(entity.Id, item); } + // delete tabs + // ... by excepting entries from db with entries from collections + List orphanPropertyTypeIds = null; if (entity.IsPropertyDirty("PropertyGroups") || entity.PropertyGroups.Any(x => x.IsDirty())) { // todo @@ -363,68 +361,96 @@ AND umbracoNode.id <> @id", // (all gone) // delete tabs that do not exist anymore - // get the tabs that are currently existing (in the db) - // get the tabs that we want, now - // and derive the tabs that we want to delete + // get the tabs that are currently existing (in the db), get the tabs that we want, + // now, and derive the tabs that we want to delete var existingPropertyGroups = Database.Fetch("WHERE contentTypeNodeId = @id", new { id = entity.Id }) .Select(x => x.Id) .ToList(); var newPropertyGroups = entity.PropertyGroups.Select(x => x.Id).ToList(); - var tabsToDelete = existingPropertyGroups + var groupsToDelete = existingPropertyGroups .Except(newPropertyGroups) .ToArray(); - // move properties to generic properties, and delete the tabs - if (tabsToDelete.Length > 0) + // delete the tabs + if (groupsToDelete.Length > 0) { - Database.Update("SET propertyTypeGroupId=NULL WHERE propertyTypeGroupId IN (@ids)", new { ids = tabsToDelete }); - Database.Delete("WHERE id IN (@ids)", new { ids = tabsToDelete }); + // if the tab contains properties, take care of them + // - move them to 'generic properties' so they remain consistent + // - keep track of them, later on we'll figure out what to do with them + // see http://issues.umbraco.org/issue/U4-8663 + orphanPropertyTypeIds = Database.Fetch("WHERE propertyTypeGroupId IN (@ids)", new { ids = groupsToDelete }) + .Select(x => x.Id).ToList(); + Database.Update("SET propertyTypeGroupId=NULL WHERE propertyTypeGroupId IN (@ids)", new { ids = groupsToDelete }); + + // now we can delete the tabs + Database.Delete("WHERE id IN (@ids)", new { ids = groupsToDelete }); } } var propertyGroupFactory = new PropertyGroupFactory(entity.Id); - //Run through all groups to insert or update entries + // insert or update groups, assign properties foreach (var propertyGroup in entity.PropertyGroups) { - var tabDto = propertyGroupFactory.BuildGroupDto(propertyGroup); - int groupPrimaryKey = propertyGroup.HasIdentity - ? Database.Update(tabDto) - : Convert.ToInt32(Database.Insert(tabDto)); + // insert or update group + var groupDto = propertyGroupFactory.BuildGroupDto(propertyGroup); + var groupId = propertyGroup.HasIdentity + ? Database.Update(groupDto) + : Convert.ToInt32(Database.Insert(groupDto)); if (propertyGroup.HasIdentity == false) - propertyGroup.Id = groupPrimaryKey; //Set Id on new PropertyGroup + propertyGroup.Id = groupId; + else + groupId = propertyGroup.Id; - //Ensure that the PropertyGroup's Id is set on the PropertyTypes within a group - //unless the PropertyGroupId has already been changed. + // assign properties to the group + // (all of them, even those that have .IsPropertyDirty("PropertyGroupId") == true, + // because it should have been set to this group anyways and better be safe) foreach (var propertyType in propertyGroup.PropertyTypes) - { - if (propertyType.IsPropertyDirty("PropertyGroupId") == false) - { - var tempGroup = propertyGroup; - propertyType.PropertyGroupId = new Lazy(() => tempGroup.Id); - } - } + propertyType.PropertyGroupId = new Lazy(() => groupId); } - //Run through all PropertyTypes to insert or update entries + // insert or update properties + // all of them, no-group and in-groups foreach (var propertyType in entity.PropertyTypes) { - var tabId = propertyType.PropertyGroupId != null ? propertyType.PropertyGroupId.Value : default(int); - //If the Id of the DataType is not set, we resolve it from the db by its PropertyEditorAlias + var groupId = propertyType.PropertyGroupId?.Value ?? default(int); + // if the Id of the DataType is not set, we resolve it from the db by its PropertyEditorAlias if (propertyType.DataTypeDefinitionId == 0 || propertyType.DataTypeDefinitionId == default(int)) - { AssignDataTypeFromPropertyEditor(propertyType); - } - //validate the alias! + // validate the alias ValidateAlias(propertyType); - var propertyTypeDto = propertyGroupFactory.BuildPropertyTypeDto(tabId, propertyType); - int typePrimaryKey = propertyType.HasIdentity - ? Database.Update(propertyTypeDto) - : Convert.ToInt32(Database.Insert(propertyTypeDto)); + // insert or update property + var propertyTypeDto = propertyGroupFactory.BuildPropertyTypeDto(groupId, propertyType); + var typeId = propertyType.HasIdentity + ? Database.Update(propertyTypeDto) + : Convert.ToInt32(Database.Insert(propertyTypeDto)); if (propertyType.HasIdentity == false) - propertyType.Id = typePrimaryKey; //Set Id on new PropertyType + propertyType.Id = typeId; + else + typeId = propertyType.Id; + + // not an orphan anymore + if (orphanPropertyTypeIds != null) + orphanPropertyTypeIds.Remove(typeId); } + + // deal with orphan properties: those that were in a deleted tab, + // and have not been re-mapped to another tab or to 'generic properties' + if (orphanPropertyTypeIds != null) + foreach (var id in orphanPropertyTypeIds) + DeletePropertyType(entity.Id, id); + } + + private void DeletePropertyType(int contentTypeId, int propertyTypeId) + { + // first clear dependencies + Database.Delete("WHERE propertyTypeId = @Id", new { Id = propertyTypeId }); + Database.Delete("WHERE propertytypeid = @Id", new { Id = propertyTypeId }); + + // then delete the property type + Database.Delete("WHERE contentTypeId = @Id AND id = @PropertyTypeId", + new { Id = contentTypeId, PropertyTypeId = propertyTypeId }); } protected IEnumerable GetAllowedContentTypeIds(int id) diff --git a/src/Umbraco.Core/Persistence/Repositories/EntityContainerRepository.cs b/src/Umbraco.Core/Persistence/Repositories/EntityContainerRepository.cs index ca48911429..96a1999f4d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/EntityContainerRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/EntityContainerRepository.cs @@ -43,7 +43,7 @@ namespace Umbraco.Core.Persistence.Repositories { var sql = GetBaseQuery(false).Where(GetBaseWhereClause(), new { id = id, NodeObjectType = NodeObjectTypeId }); - var nodeDto = Database.Fetch(sql).FirstOrDefault(); + var nodeDto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); return nodeDto == null ? null : CreateEntity(nodeDto); } diff --git a/src/Umbraco.Core/Persistence/Repositories/ExternalLoginRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ExternalLoginRepository.cs index 2fa0c18ef0..2d43ee5d9f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ExternalLoginRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ExternalLoginRepository.cs @@ -51,12 +51,12 @@ namespace Umbraco.Core.Persistence.Repositories var sql = GetBaseQuery(false); sql.Where(GetBaseWhereClause(), new { Id = id }); - var macroDto = Database.Fetch(sql).FirstOrDefault(); - if (macroDto == null) + var dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); + if (dto == null) return null; var factory = new ExternalLoginFactory(); - var entity = factory.BuildEntity(macroDto); + var entity = factory.BuildEntity(dto); //on initial construction we don't want to have dirty properties tracked // http://issues.umbraco.org/issue/U4-1946 diff --git a/src/Umbraco.Core/Persistence/Repositories/FileRepository.cs b/src/Umbraco.Core/Persistence/Repositories/FileRepository.cs index 32af5a55ad..e4573d85ec 100644 --- a/src/Umbraco.Core/Persistence/Repositories/FileRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/FileRepository.cs @@ -217,10 +217,20 @@ namespace Umbraco.Core.Persistence.Repositories protected string GetFileContent(string filename) { - using (var stream = FileSystem.OpenFile(filename)) - using (var reader = new StreamReader(stream, Encoding.UTF8, true)) + if (FileSystem.FileExists(filename) == false) + return null; + + try { - return reader.ReadToEnd(); + using (var stream = FileSystem.OpenFile(filename)) + using (var reader = new StreamReader(stream, Encoding.UTF8, true)) + { + return reader.ReadToEnd(); + } + } + catch + { + return null; // deal with race conds } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/INotificationsRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/INotificationsRepository.cs index 72a968890b..84685538d0 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/INotificationsRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/INotificationsRepository.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models.Membership; @@ -13,6 +14,7 @@ namespace Umbraco.Core.Persistence.Repositories int DeleteNotifications(IUser user, IEntity entity); IEnumerable GetEntityNotifications(IEntity entity); IEnumerable GetUserNotifications(IUser user); + IEnumerable GetUsersNotifications(IEnumerable userIds, string action, IEnumerable nodeIds, Guid objectType); IEnumerable SetNotifications(IUser user, IEntity entity, string[] actions); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IPartialViewRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IPartialViewRepository.cs index 3fbcdd3283..7c8391a77a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IPartialViewRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IPartialViewRepository.cs @@ -1,4 +1,5 @@ -using Umbraco.Core.Models; +using System.IO; +using Umbraco.Core.Models; namespace Umbraco.Core.Persistence.Repositories { @@ -7,5 +8,7 @@ namespace Umbraco.Core.Persistence.Repositories void AddFolder(string folderPath); void DeleteFolder(string folderPath); bool ValidatePartialView(IPartialView partialView); + Stream GetFileContentStream(string filepath); + void SetFileContent(string filepath, Stream content); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRedirectUrlRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRedirectUrlRepository.cs index 82f2e0e516..adf0816196 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRedirectUrlRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRedirectUrlRepository.cs @@ -7,7 +7,7 @@ namespace Umbraco.Core.Persistence.Repositories /// /// Defines the repository. /// - public interface IRedirectUrlRepository : IRepositoryQueryable + public interface IRedirectUrlRepository : IRepositoryQueryable { /// /// Gets a redirect url. @@ -21,7 +21,7 @@ namespace Umbraco.Core.Persistence.Repositories /// Deletes a redirect url. /// /// The redirect url identifier. - void Delete(int id); + void Delete(Guid id); /// /// Deletes all redirect urls. @@ -66,5 +66,15 @@ namespace Umbraco.Core.Persistence.Repositories /// The total count of redirect urls. /// The redirect urls. IEnumerable GetAllUrls(int rootContentId, long pageIndex, int pageSize, out long total); + + /// + /// Searches for all redirect urls that contain a given search term in their URL property. + /// + /// The term to search for. + /// The page index. + /// The page size. + /// The total count of redirect urls. + /// The redirect urls. + IEnumerable SearchUrls(string searchTerm, long pageIndex, int pageSize, out long total); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRepositoryVersionable.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRepositoryVersionable.cs index 6aa9336377..86e267721f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRepositoryVersionable.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRepositoryVersionable.cs @@ -25,11 +25,19 @@ namespace Umbraco.Core.Persistence.Repositories int CountDescendants(int parentId, string contentTypeAlias = null); /// - /// Gets a list of all versions for an . + /// Gets a list of all versions for an ordered so latest is first /// /// Id of the to retrieve versions from /// An enumerable list of the same object with different versions - IEnumerable GetAllVersions(int id); + IEnumerable GetAllVersions(int id); + + /// + /// Gets a list of all version Ids for the given content item + /// + /// + /// The maximum number of rows to return + /// + IEnumerable GetVersionIds(int id, int maxRows); /// /// Gets a specific version of an . diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IScriptRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IScriptRepository.cs index 9d7bfd2e36..eacb818faa 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IScriptRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IScriptRepository.cs @@ -1,9 +1,12 @@ -using Umbraco.Core.Models; +using System.IO; +using Umbraco.Core.Models; namespace Umbraco.Core.Persistence.Repositories { public interface IScriptRepository : IRepository { bool ValidateScript(Script script); + Stream GetFileContentStream(string filepath); + void SetFileContent(string filepath, Stream content); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IStylesheetRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IStylesheetRepository.cs index 1b2bcbe3eb..323f11d339 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IStylesheetRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IStylesheetRepository.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.IO; using Umbraco.Core.Models; namespace Umbraco.Core.Persistence.Repositories @@ -6,5 +6,7 @@ namespace Umbraco.Core.Persistence.Repositories public interface IStylesheetRepository : IRepository { bool ValidateStylesheet(Stylesheet stylesheet); + Stream GetFileContentStream(string filepath); + void SetFileContent(string filepath, Stream content); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/ITemplateRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/ITemplateRepository.cs index 3faba9e282..59845f53f0 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/ITemplateRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/ITemplateRepository.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.IO; using Umbraco.Core.Models; namespace Umbraco.Core.Persistence.Repositories @@ -57,5 +58,19 @@ namespace Umbraco.Core.Persistence.Repositories /// to validate /// True if Script is valid, otherwise false bool ValidateTemplate(ITemplate template); + + /// + /// Gets the content of a template as a stream. + /// + /// The filesystem path to the template. + /// The content of the template. + Stream GetFileContentStream(string filepath); + + /// + /// Sets the content of a template. + /// + /// The filesystem path to the template. + /// The content of the template. + void SetFileContent(string filepath, Stream content); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IXsltFileRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IXsltFileRepository.cs new file mode 100644 index 0000000000..8bff985400 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IXsltFileRepository.cs @@ -0,0 +1,12 @@ +using System.IO; +using Umbraco.Core.Models; + +namespace Umbraco.Core.Persistence.Repositories +{ + public interface IXsltFileRepository : IRepository + { + bool ValidateXsltFile(XsltFile xsltFile); + Stream GetFileContentStream(string filepath); + void SetFileContent(string filepath, Stream content); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs index b78c636fff..75794912ac 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs @@ -58,7 +58,7 @@ namespace Umbraco.Core.Persistence.Repositories sql.Where(GetBaseWhereClause(), new { Id = id }); sql.OrderByDescending(x => x.VersionDate); - var dto = Database.Fetch(sql).FirstOrDefault(); + var dto = Database.Fetch(sql.SelectTop(1)).FirstOrDefault(); if (dto == null) return null; @@ -369,6 +369,7 @@ namespace Umbraco.Core.Persistence.Repositories { foreach (var property in entity.Properties) { + if (keyDictionary.ContainsKey(property.PropertyTypeId) == false) continue; property.Id = keyDictionary[property.PropertyTypeId]; } } @@ -420,7 +421,7 @@ namespace Umbraco.Core.Persistence.Repositories } return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, - MapQueryDtos, orderBy, orderDirection, orderBySystemField, + MapQueryDtos, orderBy, orderDirection, orderBySystemField, "cmsContentVersion", filterSql); } diff --git a/src/Umbraco.Core/Persistence/Repositories/MemberGroupRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MemberGroupRepository.cs index 2210c981c4..b2a361dc10 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MemberGroupRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MemberGroupRepository.cs @@ -29,7 +29,7 @@ namespace Umbraco.Core.Persistence.Repositories var sql = GetBaseQuery(false); sql.Where(GetBaseWhereClause(), new { Id = id }); - var dto = Database.Fetch(sql).FirstOrDefault(); + var dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); return dto == null ? null : _modelFactory.BuildEntity(dto); } diff --git a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs index 139f3b7d6a..9d0517bda2 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Text; using System.Xml.Linq; using NPoco; using Umbraco.Core.Cache; @@ -56,7 +57,7 @@ namespace Umbraco.Core.Persistence.Repositories sql.Where(GetBaseWhereClause(), new { Id = id }); sql.OrderByDescending(x => x.VersionDate); - var dto = Database.Fetch(sql).FirstOrDefault(); + var dto = Database.Fetch(sql.SelectTop(1)).FirstOrDefault(); if (dto == null) return null; @@ -374,6 +375,8 @@ namespace Umbraco.Core.Persistence.Repositories { foreach (var property in ((Member)entity).Properties) { + if (keyDictionary.ContainsKey(property.PropertyTypeId) == false) continue; + property.Id = keyDictionary[property.PropertyTypeId]; } } @@ -554,14 +557,31 @@ namespace Umbraco.Core.Persistence.Repositories { var filterSql = filter.IsNullOrWhiteSpace() ? null - : Sql().Append("AND ((umbracoNode. " + SqlSyntax.GetQuotedColumnName("text") + " LIKE @0) " + - "OR (cmsMember.LoginName LIKE @0))", "%" + filter + "%"); + : Sql().Append(GetPagedResultsByQueryWhere(), $"%{filter}%"); + + // note: need to test whether NPoco gets confused by the same parameter being used twice, + // as PetaPoco supposedly was, in which case we'd need to use two parameters. in any case, + // better to create the query text only once! return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, - MapQueryDtos, orderBy, orderDirection, orderBySystemField, + MapQueryDtos, orderBy, orderDirection, orderBySystemField, "cmsMember", filterSql); } + private string _pagedResultsByQueryWhere; + + private string GetPagedResultsByQueryWhere() + { + if (_pagedResultsByQueryWhere == null) + _pagedResultsByQueryWhere = " AND (" + + $"({SqlSyntax.GetQuotedTableName("umbracoNode")}.{SqlSyntax.GetQuotedColumnName("text")} LIKE @0)" + + " OR " + + $"({SqlSyntax.GetQuotedTableName("cmsMember")}.{SqlSyntax.GetQuotedColumnName("LoginName")} LIKE @0)" + + ")"; + + return _pagedResultsByQueryWhere; + } + protected override string GetDatabaseFieldNameForOrderBy(string orderBy) { //Some custom ones diff --git a/src/Umbraco.Core/Persistence/Repositories/MigrationEntryRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MigrationEntryRepository.cs index baf6a0647a..d47f579ea4 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MigrationEntryRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MigrationEntryRepository.cs @@ -29,7 +29,7 @@ namespace Umbraco.Core.Persistence.Repositories var sql = GetBaseQuery(false); sql.Where(GetBaseWhereClause(), new { Id = id }); - var dto = Database.First(sql); + var dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); if (dto == null) return null; diff --git a/src/Umbraco.Core/Persistence/Repositories/NotificationsRepository.cs b/src/Umbraco.Core/Persistence/Repositories/NotificationsRepository.cs index 936e43839b..2a7ef57442 100644 --- a/src/Umbraco.Core/Persistence/Repositories/NotificationsRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/NotificationsRepository.cs @@ -20,6 +20,26 @@ namespace Umbraco.Core.Persistence.Repositories _unitOfWork = unitOfWork; } + public IEnumerable GetUsersNotifications(IEnumerable userIds, string action, IEnumerable nodeIds, Guid objectType) + { + var nodeIdsA = nodeIds.ToArray(); + var sql = _unitOfWork.Database.Sql() + .Select("DISTINCT umbracoNode.id nodeId, umbracoUser.id userId, umbracoNode.nodeObjectType, umbracoUser2NodeNotify.action") + .From() + .InnerJoin().On(left => left.NodeId, right => right.NodeId) + .InnerJoin().On(left => left.UserId, right => right.Id) + .Where(x => x.NodeObjectType == objectType) + .Where(x => x.Disabled == false) // only approved users + .Where(x => x.Action == action); // on the specified action + if (nodeIdsA.Length > 0) + sql + .WhereIn(x => x.NodeId, nodeIdsA); // for the specified nodes + sql + .OrderBy(x => x.Id) + .OrderBy(dto => dto.NodeId); + return _unitOfWork.Database.Fetch(sql).Select(x => new Notification(x.nodeId, x.userId, x.action, objectType)); + } + public IEnumerable GetUserNotifications(IUser user) { var sql = _unitOfWork.Database.Sql() diff --git a/src/Umbraco.Core/Persistence/Repositories/PartialViewMacroRepository.cs b/src/Umbraco.Core/Persistence/Repositories/PartialViewMacroRepository.cs index d10e4add02..806bf858a2 100644 --- a/src/Umbraco.Core/Persistence/Repositories/PartialViewMacroRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/PartialViewMacroRepository.cs @@ -1,5 +1,4 @@ -using System.Threading; -using LightInject; +using LightInject; using Umbraco.Core.IO; using Umbraco.Core.Models; using Umbraco.Core.Persistence.UnitOfWork; diff --git a/src/Umbraco.Core/Persistence/Repositories/PartialViewRepository.cs b/src/Umbraco.Core/Persistence/Repositories/PartialViewRepository.cs index 660ccdd983..366a4265ea 100644 --- a/src/Umbraco.Core/Persistence/Repositories/PartialViewRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/PartialViewRepository.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; @@ -108,6 +107,25 @@ namespace Umbraco.Core.Persistence.Repositories return isValidPath && isValidExtension; } + public Stream GetFileContentStream(string filepath) + { + if (FileSystem.FileExists(filepath) == false) return null; + + try + { + return FileSystem.OpenFile(filepath); + } + catch + { + return null; // deal with race conds + } + } + + public void SetFileContent(string filepath, Stream content) + { + FileSystem.AddFile(filepath, content, true); + } + /// /// Gets a stream that is used to write to the file /// diff --git a/src/Umbraco.Core/Persistence/Repositories/PublicAccessRepository.cs b/src/Umbraco.Core/Persistence/Repositories/PublicAccessRepository.cs index b956c42136..2b0bbb25a5 100644 --- a/src/Umbraco.Core/Persistence/Repositories/PublicAccessRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/PublicAccessRepository.cs @@ -20,8 +20,7 @@ namespace Umbraco.Core.Persistence.Repositories public PublicAccessRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, IMapperCollection mappers) : base(work, cache, logger, mappers) - { - } + { } protected override IRepositoryCachePolicy CachePolicy { @@ -50,6 +49,8 @@ namespace Umbraco.Core.Persistence.Repositories sql.Where("umbracoAccess.id IN (@ids)", new { ids = ids }); } + sql.OrderBy(x => x.NodeId); + var factory = new PublicAccessEntryFactory(); var dtos = Database.FetchOneToMany(x => x.Rules, sql); return dtos.Select(factory.BuildEntity); @@ -163,8 +164,6 @@ namespace Umbraco.Core.Persistence.Repositories protected override Guid GetEntityId(PublicAccessEntry entity) { return entity.Key; - } - - + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/RedirectUrlRepository.cs b/src/Umbraco.Core/Persistence/Repositories/RedirectUrlRepository.cs index ffed648b7a..0a5f8c0237 100644 --- a/src/Umbraco.Core/Persistence/Repositories/RedirectUrlRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/RedirectUrlRepository.cs @@ -14,7 +14,7 @@ using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Persistence.Repositories { - internal class RedirectUrlRepository : NPocoRepositoryBase, IRedirectUrlRepository + internal class RedirectUrlRepository : NPocoRepositoryBase, IRedirectUrlRepository { public RedirectUrlRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, IMapperCollection mappers) : base(work, cache, logger, mappers) @@ -25,19 +25,19 @@ namespace Umbraco.Core.Persistence.Repositories throw new NotSupportedException("This repository does not support this method."); } - protected override bool PerformExists(int id) + protected override bool PerformExists(Guid id) { return PerformGet(id) != null; } - protected override IRedirectUrl PerformGet(int id) + protected override IRedirectUrl PerformGet(Guid id) { var sql = GetBaseQuery(false).Where(x => x.Id == id); - var dto = Database.Fetch(sql).FirstOrDefault(); + var dto = Database.Fetch(sql.SelectTop(1)).FirstOrDefault(); return dto == null ? null : Map(dto); } - protected override IEnumerable PerformGetAll(params int[] ids) + protected override IEnumerable PerformGetAll(params Guid[] ids) { if (ids.Length > 2000) throw new NotSupportedException("This repository does not support more than 2000 ids."); @@ -88,7 +88,7 @@ JOIN umbracoNode ON umbracoRedirectUrl.contentKey=umbracoNode.uniqueID"); { var dto = Map(entity); Database.Insert(dto); - entity.Id = dto.Id; + entity.Id = entity.Key.GetHashCode(); } protected override void PersistUpdatedItem(IRedirectUrl entity) @@ -103,11 +103,11 @@ JOIN umbracoNode ON umbracoRedirectUrl.contentKey=umbracoNode.uniqueID"); return new RedirectUrlDto { - Id = redirectUrl.Id, + Id = redirectUrl.Key, ContentKey = redirectUrl.ContentKey, CreateDateUtc = redirectUrl.CreateDateUtc, Url = redirectUrl.Url, - UrlHash = HashUrl(redirectUrl.Url) + UrlHash = redirectUrl.Url.ToSHA1() }; } @@ -119,7 +119,8 @@ JOIN umbracoNode ON umbracoRedirectUrl.contentKey=umbracoNode.uniqueID"); try { url.DisableChangeTracking(); - url.Id = dto.Id; + url.Key = dto.Id; + url.Id = dto.Id.GetHashCode(); url.ContentId = dto.ContentId; url.ContentKey = dto.ContentKey; url.CreateDateUtc = dto.CreateDateUtc; @@ -134,7 +135,7 @@ JOIN umbracoNode ON umbracoRedirectUrl.contentKey=umbracoNode.uniqueID"); public IRedirectUrl Get(string url, Guid contentKey) { - var urlHash = HashUrl(url); + var urlHash = url.ToSHA1(); var sql = GetBaseQuery(false).Where(x => x.Url == url && x.UrlHash == urlHash && x.ContentKey == contentKey); var dto = Database.Fetch(sql).FirstOrDefault(); return dto == null ? null : Map(dto); @@ -150,14 +151,14 @@ JOIN umbracoNode ON umbracoRedirectUrl.contentKey=umbracoNode.uniqueID"); Database.Execute("DELETE FROM umbracoRedirectUrl WHERE contentKey=@contentKey", new { contentKey }); } - public void Delete(int id) + public void Delete(Guid id) { Database.Delete(id); } public IRedirectUrl GetMostRecentUrl(string url) { - var urlHash = HashUrl(url); + var urlHash = url.ToSHA1(); var sql = GetBaseQuery(false) .Where(x => x.Url == url && x.UrlHash == urlHash) .OrderByDescending(x => x.CreateDateUtc); @@ -187,7 +188,7 @@ JOIN umbracoNode ON umbracoRedirectUrl.contentKey=umbracoNode.uniqueID"); public IEnumerable GetAllUrls(int rootContentId, long pageIndex, int pageSize, out long total) { var sql = GetBaseQuery(false) - .Where("umbracoNode.path LIKE @path", new { path = "%," + rootContentId + ",%" }) + .Where(string.Format("{0}.{1} LIKE @path", SqlSyntax.GetQuotedTableName("umbracoNode"), SqlSyntax.GetQuotedColumnName("path")), new { path = "%," + rootContentId + ",%" }) .OrderByDescending(x => x.CreateDateUtc); var result = Database.Page(pageIndex + 1, pageSize, sql); total = Convert.ToInt32(result.TotalItems); @@ -196,12 +197,16 @@ JOIN umbracoNode ON umbracoRedirectUrl.contentKey=umbracoNode.uniqueID"); return rules; } - private static string HashUrl(string url) + public IEnumerable SearchUrls(string searchTerm, long pageIndex, int pageSize, out long total) { - var crypto = new MD5CryptoServiceProvider(); - var inputBytes = Encoding.UTF8.GetBytes(url); - var hashedBytes = crypto.ComputeHash(inputBytes); - return Encoding.UTF8.GetString(hashedBytes); - } + var sql = GetBaseQuery(false) + .Where(string.Format("{0}.{1} LIKE @url", SqlSyntax.GetQuotedTableName("umbracoRedirectUrl"), SqlSyntax.GetQuotedColumnName("Url")), new { url = "%" + searchTerm.Trim().ToLowerInvariant() + "%" }) + .OrderByDescending(x => x.CreateDateUtc); + var result = Database.Page(pageIndex + 1, pageSize, sql); + total = Convert.ToInt32(result.TotalItems); + + var rules = result.Items.Select(Map); + return rules; + } } } diff --git a/src/Umbraco.Core/Persistence/Repositories/RelationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/RelationRepository.cs index ca9d25a22c..ee4a4689f7 100644 --- a/src/Umbraco.Core/Persistence/Repositories/RelationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/RelationRepository.cs @@ -38,7 +38,7 @@ namespace Umbraco.Core.Persistence.Repositories var sql = GetBaseQuery(false); sql.Where(GetBaseWhereClause(), new { Id = id }); - var dto = Database.FirstOrDefault(sql); + var dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); if (dto == null) return null; diff --git a/src/Umbraco.Core/Persistence/Repositories/RelationTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/RelationTypeRepository.cs index bb73488397..a609b8c023 100644 --- a/src/Umbraco.Core/Persistence/Repositories/RelationTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/RelationTypeRepository.cs @@ -36,7 +36,7 @@ namespace Umbraco.Core.Persistence.Repositories var sql = GetBaseQuery(false); sql.Where(GetBaseWhereClause(), new { Id = id }); - var dto = Database.FirstOrDefault(sql); + var dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); if (dto == null) return null; diff --git a/src/Umbraco.Core/Persistence/Repositories/ScriptRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ScriptRepository.cs index 215e1ffc6d..cda5de6d80 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ScriptRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ScriptRepository.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using LightInject; using Umbraco.Core.Configuration.UmbracoSettings; @@ -110,6 +111,25 @@ namespace Umbraco.Core.Persistence.Repositories return isValidPath && isValidExtension; } + public Stream GetFileContentStream(string filepath) + { + if (FileSystem.FileExists(filepath) == false) return null; + + try + { + return FileSystem.OpenFile(filepath); + } + catch + { + return null; // deal with race conds + } + } + + public void SetFileContent(string filepath, Stream content) + { + FileSystem.AddFile(filepath, content, true); + } + #endregion } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/StylesheetRepository.cs b/src/Umbraco.Core/Persistence/Repositories/StylesheetRepository.cs index 9c84a38a12..b3dd90725e 100644 --- a/src/Umbraco.Core/Persistence/Repositories/StylesheetRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/StylesheetRepository.cs @@ -125,6 +125,25 @@ namespace Umbraco.Core.Persistence.Repositories return isValidPath && isValidExtension; } + public Stream GetFileContentStream(string filepath) + { + if (FileSystem.FileExists(filepath) == false) return null; + + try + { + return FileSystem.OpenFile(filepath); + } + catch + { + return null; // deal with race conds + } + } + + public void SetFileContent(string filepath, Stream content) + { + FileSystem.AddFile(filepath, content, true); + } + #endregion } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/TagRepository.cs b/src/Umbraco.Core/Persistence/Repositories/TagRepository.cs index 9c789d1626..77224a0eaa 100644 --- a/src/Umbraco.Core/Persistence/Repositories/TagRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/TagRepository.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; using NPoco; using Umbraco.Core.Cache; using Umbraco.Core.Logging; @@ -29,7 +28,7 @@ namespace Umbraco.Core.Persistence.Repositories var sql = GetBaseQuery(false); sql.Where(GetBaseWhereClause(), new { Id = id }); - var tagDto = Database.Fetch(sql).FirstOrDefault(); + var tagDto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); if (tagDto == null) return null; @@ -550,7 +549,7 @@ namespace Umbraco.Core.Persistence.Repositories var array = tagsToInsert .Select(tag => - string.Format("select '{0}' as Tag, '{1}' as " + SqlSyntax.GetQuotedColumnName("group") + @"", + string.Format("select N'{0}' as Tag, '{1}' as " + SqlSyntax.GetQuotedColumnName("group") + @"", NPocoDatabaseExtensions.EscapeAtSymbols(tag.Text.Replace("'", "''")), tag.Group)) .ToArray(); return "(" + string.Join(" union ", array).Replace(" ", " ") + ") as TagSet"; diff --git a/src/Umbraco.Core/Persistence/Repositories/TaskRepository.cs b/src/Umbraco.Core/Persistence/Repositories/TaskRepository.cs index 913c04c3e6..0585540f09 100644 --- a/src/Umbraco.Core/Persistence/Repositories/TaskRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/TaskRepository.cs @@ -30,7 +30,7 @@ namespace Umbraco.Core.Persistence.Repositories var sql = GetBaseQuery(false); sql.Where(GetBaseWhereClause(), new { Id = id }); - var taskDto = Database.Fetch(sql).FirstOrDefault(); + var taskDto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); if (taskDto == null) return null; diff --git a/src/Umbraco.Core/Persistence/Repositories/TaskTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/TaskTypeRepository.cs index 18e659cb49..442daed787 100644 --- a/src/Umbraco.Core/Persistence/Repositories/TaskTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/TaskTypeRepository.cs @@ -28,7 +28,7 @@ namespace Umbraco.Core.Persistence.Repositories var sql = GetBaseQuery(false); sql.Where(GetBaseWhereClause(), new { Id = id }); - var taskDto = Database.Fetch(sql).FirstOrDefault(); + var taskDto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); if (taskDto == null) return null; diff --git a/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs b/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs index 3a9fdca352..4feff6db71 100644 --- a/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs @@ -44,7 +44,7 @@ namespace Umbraco.Core.Persistence.Repositories _viewsFileSystem = viewFileSystem; _templateConfig = templateConfig; _viewHelper = new ViewHelper(_viewsFileSystem); - _masterPageHelper = new MasterPageHelper(_masterpagesFileSystem); + _masterPageHelper = new MasterPageHelper(_masterpagesFileSystem); } protected override IRepositoryCachePolicy CachePolicy @@ -198,29 +198,7 @@ namespace Umbraco.Core.Persistence.Repositories template.Path = nodeDto.Path; //now do the file work - - if (DetermineTemplateRenderingEngine(entity) == RenderingEngine.Mvc) - { - var result = _viewHelper.CreateView(template, true); - if (result != entity.Content) - { - entity.Content = result; - //re-persist it... though we don't really care about the templates in the db do we??!! - dto.Design = result; - Database.Update(dto); - } - } - else - { - var result = _masterPageHelper.CreateMasterPage(template, this, true); - if (result != entity.Content) - { - entity.Content = result; - //re-persist it... though we don't really care about the templates in the db do we??!! - dto.Design = result; - Database.Update(dto); - } - } + SaveFile(template, dto); template.ResetDirtyProperties(); @@ -270,29 +248,7 @@ namespace Umbraco.Core.Persistence.Repositories template.IsMasterTemplate = axisDefs.Any(x => x.ParentId == dto.NodeId); //now do the file work - - if (DetermineTemplateRenderingEngine(entity) == RenderingEngine.Mvc) - { - var result = _viewHelper.UpdateViewFile(entity, originalAlias); - if (result != entity.Content) - { - entity.Content = result; - //re-persist it... though we don't really care about the templates in the db do we??!! - dto.Design = result; - Database.Update(dto); - } - } - else - { - var result = _masterPageHelper.UpdateMasterPageFile(entity, originalAlias, this); - if (result != entity.Content) - { - entity.Content = result; - //re-persist it... though we don't really care about the templates in the db do we??!! - dto.Design = result; - Database.Update(dto); - } - } + SaveFile((Template) entity, dto, originalAlias); entity.ResetDirtyProperties(); @@ -301,6 +257,43 @@ namespace Umbraco.Core.Persistence.Repositories template.GetFileContent = file => GetFileContent((Template) file, false); } + private void SaveFile(Template template, TemplateDto dto, string originalAlias = null) + { + string content; + + var templateOnDisk = template as TemplateOnDisk; + if (templateOnDisk != null && templateOnDisk.IsOnDisk) + { + // if "template on disk" load content from disk + content = _viewHelper.GetFileContents(template); + } + else + { + // else, create or write template.Content to disk + if (DetermineTemplateRenderingEngine(template) == RenderingEngine.Mvc) + { + content = originalAlias == null + ? _viewHelper.CreateView(template, true) + : _viewHelper.UpdateViewFile(template, originalAlias); + } + else + { + content = originalAlias == null + ? _masterPageHelper.CreateMasterPage(template, this, true) + : _masterPageHelper.UpdateMasterPageFile(template, originalAlias, this); + } + } + + // once content has been set, "template on disk" are not "on disk" anymore + template.Content = content; + + if (dto.Design == content) return; + dto.Design = content; + Database.Update(dto); // though... we don't care about the db value really??!! + + SetVirtualPath(template); + } + protected override void PersistDeletedItem(ITemplate entity) { var deletes = GetDeleteClauses().ToArray(); @@ -396,6 +389,50 @@ namespace Umbraco.Core.Persistence.Repositories return template; } + private void SetVirtualPath(ITemplate template) + { + var path = template.OriginalPath; + if (string.IsNullOrWhiteSpace(path)) + { + // we need to discover the path + path = string.Concat(template.Alias, ".cshtml"); + if (_viewsFileSystem.FileExists(path)) + { + template.VirtualPath = _viewsFileSystem.GetUrl(path); + return; + } + path = string.Concat(template.Alias, ".vbhtml"); + if (_viewsFileSystem.FileExists(path)) + { + template.VirtualPath = _viewsFileSystem.GetUrl(path); + return; + } + path = string.Concat(template.Alias, ".master"); + if (_masterpagesFileSystem.FileExists(path)) + { + template.VirtualPath = _masterpagesFileSystem.GetUrl(path); + return; + } + } + else + { + // we know the path already + var ext = Path.GetExtension(path); + switch (ext) + { + case ".cshtml": + case ".vbhtml": + template.VirtualPath = _viewsFileSystem.GetUrl(path); + return; + case ".master": + template.VirtualPath = _masterpagesFileSystem.GetUrl(path); + return; + } + } + + template.VirtualPath = string.Empty; // file not found... + } + private string GetFileContent(ITemplate template, bool init) { var path = template.OriginalPath; @@ -423,20 +460,10 @@ namespace Umbraco.Core.Persistence.Repositories return GetFileContent(template, _viewsFileSystem, path, init); case ".master": return GetFileContent(template, _masterpagesFileSystem, path, init); - default: - return string.Empty; } } - var fsname = string.Concat(template.Alias, ".cshtml"); - if (_viewsFileSystem.FileExists(fsname)) - return GetFileContent(template, _viewsFileSystem, fsname, init); - fsname = string.Concat(template.Alias, ".vbhtml"); - if (_viewsFileSystem.FileExists(fsname)) - return GetFileContent(template, _viewsFileSystem, fsname, init); - fsname = string.Concat(template.Alias, ".master"); - if (_masterpagesFileSystem.FileExists(fsname)) - return GetFileContent(template, _masterpagesFileSystem, fsname, init); + template.VirtualPath = string.Empty; // file not found... return string.Empty; } @@ -471,6 +498,45 @@ namespace Umbraco.Core.Persistence.Repositories } } + public Stream GetFileContentStream(string filepath) + { + var fs = GetFileSystem(filepath); + if (fs.FileExists(filepath) == false) return null; + + try + { + return GetFileSystem(filepath).OpenFile(filepath); + } + catch + { + return null; // deal with race conds + } + } + + public void SetFileContent(string filepath, Stream content) + { + GetFileSystem(filepath).AddFile(filepath, content, true); + } + + private IFileSystem GetFileSystem(string filepath) + { + var ext = Path.GetExtension(filepath); + IFileSystem fs; + switch (ext) + { + case ".cshtml": + case ".vbhtml": + fs = _viewsFileSystem; + break; + case ".master": + fs = _masterpagesFileSystem; + break; + default: + throw new Exception("Unsupported extension " + ext + "."); + } + return fs; + } + #region Implementation of ITemplateRepository public ITemplate Get(string alias) @@ -536,7 +602,7 @@ namespace Umbraco.Core.Persistence.Repositories //return the list - it will be naturally ordered by level return descendants; } - + public IEnumerable GetDescendants(string alias) { var all = base.GetAll().ToArray(); diff --git a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs index 6140b06489..5103da91f0 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs @@ -331,48 +331,46 @@ namespace Umbraco.Core.Persistence.Repositories { if (orderBy == null) throw new ArgumentNullException(nameof(orderBy)); - var sql = Sql() - .SelectAll() - .From(); - - Sql resultQuery; - if (query != null) - { - var translator = new SqlTranslator(sql, query); - resultQuery = translator.Translate(); - } - else - { - resultQuery = sql; - } - - //get the referenced column name + // get the referenced column name and find the corresp mapped column name var expressionMember = ExpressionHelper.GetMemberInfo(orderBy); - //now find the mapped column name var mapper = QueryFactory.Mappers[typeof(IUser)]; var mappedField = mapper.Map(SqlSyntax, expressionMember.Name); + if (mappedField.IsNullOrWhiteSpace()) - { throw new ArgumentException("Could not find a mapping for the column specified in the orderBy clause"); - } - //need to ensure the order by is in brackets, see: https://github.com/toptensoftware/PetaPoco/issues/177 - resultQuery.OrderBy(string.Format("({0})", mappedField)); - var pagedResult = Database.Page(pageIndex + 1, pageSize, resultQuery); + var sql = Sql() + .Select("umbracoUser.Id") + .From(); - totalRecords = Convert.ToInt32(pagedResult.TotalItems); + var idsQuery = query == null ? sql : new SqlTranslator(sql, query).Translate(); + + // need to ensure the order by is in brackets, see: https://github.com/toptensoftware/PetaPoco/issues/177 + idsQuery.OrderBy("(" + mappedField + ")"); + var page = Database.Page(pageIndex + 1, pageSize, idsQuery); + totalRecords = Convert.ToInt32(page.TotalItems); - //now that we have the user dto's we need to construct true members from the list. if (totalRecords == 0) - { return Enumerable.Empty(); - } - var ids = pagedResult.Items.Select(x => x.Id).ToArray(); - var result = ids.Length == 0 ? Enumerable.Empty() : GetAll(ids); + // now get the actual users and ensure they are ordered properly (same clause) + var ids = page.Items.ToArray(); + return ids.Length == 0 ? Enumerable.Empty() : GetAll(ids).OrderBy(orderBy.Compile()); + } - //now we need to ensure this result is also ordered by the same order by clause - return result.OrderBy(orderBy.Compile()); + internal IEnumerable GetNextUsers(int id, int count) + { + var idsQuery = Sql() + .Select("umbracoUser.Id") + .From() + .Where(x => x.Id >= id) + .OrderBy(x => x.Id); + + // first page is index 1, not zero + var ids = Database.Page(1, count, idsQuery).Items.ToArray(); + + // now get the actual users and ensure they are ordered properly (same clause) + return ids.Length == 0 ? Enumerable.Empty() : GetAll(ids).OrderBy(x => x.Id); } /// diff --git a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs index 699078d174..61cf01c422 100644 --- a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; -using System.Threading.Tasks; using NPoco; using Umbraco.Core.Cache; using Umbraco.Core.Configuration.UmbracoSettings; @@ -21,7 +20,6 @@ using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Persistence.UnitOfWork; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; -using Umbraco.Core.IO; using Umbraco.Core.Persistence.Mappers; namespace Umbraco.Core.Persistence.Repositories @@ -29,7 +27,7 @@ namespace Umbraco.Core.Persistence.Repositories // this cannot be inside VersionableRepositoryBase because that class is static internal static class VersionableRepositoryBaseAliasRegex { - private readonly static Dictionary Regexes = new Dictionary(); + private static readonly Dictionary Regexes = new Dictionary(); public static Regex For(ISqlSyntaxProvider sqlSyntax) { @@ -50,18 +48,23 @@ namespace Umbraco.Core.Persistence.Repositories where TEntity : class, IAggregateRoot where TRepository : class, IRepository { - private readonly IContentSection _contentSection; + //private readonly IContentSection _contentSection; protected VersionableRepositoryBase(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, IContentSection contentSection, IMapperCollection mappers) : base(work, cache, logger, mappers) { - _contentSection = contentSection; + //_contentSection = contentSection; } protected abstract TRepository Instance { get; } #region IRepositoryVersionable Implementation + /// + /// Gets a list of all versions for an ordered so latest is first + /// + /// Id of the to retrieve versions from + /// An enumerable list of the same object with different versions public virtual IEnumerable GetAllVersions(int id) { var sql = Sql() @@ -79,6 +82,28 @@ namespace Umbraco.Core.Persistence.Repositories return dtos.Select(x => GetByVersion(x.VersionId)); } + /// + /// Gets a list of all version Ids for the given content item ordered so latest is first + /// + /// + /// The maximum number of rows to return + /// + public virtual IEnumerable GetVersionIds(int id, int maxRows) + { + var sql = Sql(); + sql.Select("cmsDocument.versionId") + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .Where(x => x.NodeObjectType == NodeObjectTypeId) + .Where(x => x.NodeId == id) + .OrderByDescending(x => x.UpdateDate); + + return Database.Fetch(SqlSyntax.SelectTop(sql, maxRows)); + } + public virtual void DeleteVersion(Guid versionId) { var dto = Database.FirstOrDefault("WHERE versionId = @VersionId", new { VersionId = versionId }); @@ -251,7 +276,7 @@ namespace Umbraco.Core.Persistence.Repositories return entity.Properties.Any(x => x.TagSupport.Enable); } - private Sql PrepareSqlForPagedResults(Sql sql, Sql filterSql, string orderBy, Direction orderDirection, bool orderBySystemField) + private Sql PrepareSqlForPagedResults(Sql sql, Sql filterSql, string orderBy, Direction orderDirection, bool orderBySystemField, string table) { if (filterSql == null && string.IsNullOrEmpty(orderBy)) return sql; @@ -268,8 +293,8 @@ namespace Umbraco.Core.Persistence.Repositories // else apply sort var dbfield = orderBySystemField - ? PrepareSqlForPagedResultsWithSystemField(ref psql, orderBy) - : PrepareSqlForPagedResultsWithNonSystemField(ref psql, orderBy); + ? GetOrderBySystemField(ref psql, orderBy) + : GetOrderByNonSystemField(ref psql, orderBy, table); if (orderDirection == Direction.Ascending) psql.OrderBy(dbfield); @@ -279,7 +304,7 @@ namespace Umbraco.Core.Persistence.Repositories return psql; } - private string PrepareSqlForPagedResultsWithSystemField(ref Sql sql, string orderBy) + private string GetOrderBySystemField(ref Sql sql, string orderBy) { // get the database field eg "[table].[column]" var dbfield = GetDatabaseFieldNameForOrderBy(orderBy); @@ -304,35 +329,71 @@ namespace Umbraco.Core.Persistence.Repositories return dbfield; } - private string PrepareSqlForPagedResultsWithNonSystemField(ref Sql sql, string orderBy) + private string GetOrderByNonSystemField(ref Sql sql, string orderBy, string table) { - // Sorting by a custom field, so set-up sub-query for ORDER BY clause to pull through valie + // Sorting by a custom field, so set-up sub-query for ORDER BY clause to pull through value // from most recent content version for the given order by field var sortedInt = string.Format(SqlSyntax.ConvertIntegerToOrderableString, "dataInt"); var sortedDate = string.Format(SqlSyntax.ConvertDateToOrderableString, "dataDate"); - var sortedString = $"COALESCE({"dataNvarchar"},'')"; // assuming COALESCE is ok for all syntaxes + var sortedString = "COALESCE(dataNvarchar,'')"; // assuming COALESCE is ok for all syntaxes var sortedDecimal = string.Format(SqlSyntax.ConvertDecimalToOrderableString, "dataDecimal"); - var innerJoinTempTable = $@"INNER JOIN ( - SELECT CASE - WHEN dataInt Is Not Null THEN {sortedInt} + // variable query fragments that depend on what we are querying + string andVersion, andNewest, idField; + switch (table) + { + case "cmsDocument": + andVersion = " AND cpd.versionId = cd.versionId"; + andNewest = " AND cd.newest = 1"; + idField = "nodeId"; + break; + case "cmsMember": + andVersion = string.Empty; + andNewest = string.Empty; + idField = "nodeId"; + break; + case "cmsContentVersion": + andVersion = " AND cpd.versionId = cd.versionId"; + andNewest = string.Empty; + idField = "contentId"; + break; + default: + throw new NotSupportedException($"Table {table} is not supported."); + } + + // needs to be an outer join since there's no guarantee that any of the nodes have values for this property + var outerJoinTempTable = $@"LEFT OUTER JOIN ( + SELECT CASE + WHEN dataInt Is Not Null THEN {sortedInt} WHEN dataDecimal Is Not Null THEN {sortedDecimal} WHEN dataDate Is Not Null THEN {sortedDate} ELSE {sortedString} - END AS CustomPropVal, - cd.nodeId AS CustomPropValContentId - FROM cmsDocument cd - INNER JOIN cmsPropertyData cpd ON cpd.contentNodeId = cd.nodeId AND cpd.versionId = cd.versionId + END AS CustomPropVal, + cd.{idField} AS CustomPropValContentId + FROM {table} cd + INNER JOIN cmsPropertyData cpd ON cpd.contentNodeId = cd.{idField}{andVersion} INNER JOIN cmsPropertyType cpt ON cpt.Id = cpd.propertytypeId - WHERE cpt.Alias = @2 AND cd.newest = 1) AS CustomPropData - ON CustomPropData.CustomPropValContentId = umbracoNode.id -"; + WHERE cpt.Alias = @{sql.Arguments.Length}{andNewest}) AS CustomPropData + ON CustomPropData.CustomPropValContentId = umbracoNode.id"; + + // insert this just above the first LEFT OUTER JOIN (for cmsDocument) or the last WHERE (everything else) + string newSql; + if (table == "document") + { + // insert the SQL fragment just above the LEFT OUTER JOIN [cmsDocument] [cmsDocument2] ... + // ensure it's there, 'cos, someone's going to edit the query, inevitably! + var pos = sql.SQL.InvariantIndexOf("LEFT OUTER JOIN"); + if (pos < 0) throw new Exception("Oops, LEFT OUTER JOIN not found."); + newSql = sql.SQL.Insert(pos, outerJoinTempTable); + } + else // anything else (see above) + { + // else same above WHERE + var pos = sql.SQL.InvariantIndexOf("WHERE"); + if (pos < 0) throw new Exception("Oops, WHERE not found."); + newSql = sql.SQL.Insert(pos, outerJoinTempTable); + } - // insert the SQL fragment just above the LEFT OUTER JOIN [cmsDocument] [cmsDocument2] ... - // ensure it's there, 'cos, someone's going to edit the query, eventually! - var pos = sql.SQL.InvariantIndexOf("LEFT OUTER JOIN"); - if (pos < 0) throw new Exception("Oops, LEFT OUTER JOIN not found."); - var newSql = sql.SQL.Insert(pos, innerJoinTempTable); var newArgs = sql.Arguments.ToList(); newArgs.Add(orderBy); @@ -342,12 +403,19 @@ namespace Umbraco.Core.Persistence.Repositories sql = new Sql(sql.SqlContext, newSql, newArgs.ToArray()); + // no matter what we always MUST order the result also by umbracoNode.id to ensure that all records being ordered by are unique. + // if we do not do this then we end up with issues where we are ordering by a field that has duplicate values (i.e. the 'text' column + // is empty for many nodes) + // see: http://issues.umbraco.org/issue/U4-8831 + sql.OrderBy("umbracoNode.id"); + + // and order by the custom field return "CustomPropData.CustomPropVal"; } protected IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, Func, IEnumerable> mapper, - string orderBy, Direction orderDirection, bool orderBySystemField, + string orderBy, Direction orderDirection, bool orderBySystemField, string table, Sql filterSql = null) { if (orderBy == null) throw new ArgumentNullException(nameof(orderBy)); @@ -359,7 +427,7 @@ namespace Umbraco.Core.Persistence.Repositories var sqlNodeIds = translator.Translate(); // sort and filter - sqlNodeIds = PrepareSqlForPagedResults(sqlNodeIds, filterSql, orderBy, orderDirection, orderBySystemField); + sqlNodeIds = PrepareSqlForPagedResults(sqlNodeIds, filterSql, orderBy, orderDirection, orderBySystemField, table); // get a page of DTOs and the total count var pagedResult = Database.Page(pageIndex + 1, pageSize, sqlNodeIds); @@ -426,17 +494,6 @@ namespace Umbraco.Core.Persistence.Repositories var propertyFactory = new PropertyFactory(compositionProperties, def.Version, def.Id, def.CreateDate, def.VersionDate); var properties = propertyFactory.BuildEntity(propertyDataDtos.ToArray()).ToArray(); - var newProperties = properties.Where(x => x.HasIdentity == false && x.PropertyType.HasIdentity); - - foreach (var property in newProperties) - { - var propertyDataDto = new PropertyDataDto { NodeId = def.Id, PropertyTypeId = property.PropertyTypeId, VersionId = def.Version }; - int primaryKey = Convert.ToInt32(Database.Insert(propertyDataDto)); - - property.Version = def.Version; - property.Id = primaryKey; - } - foreach (var property in properties) { //NOTE: The benchmarks run with and without the following code show very little change so this is not a perf bottleneck @@ -514,6 +571,8 @@ namespace Umbraco.Core.Persistence.Repositories return GetDatabaseFieldNameForOrderBy("umbracoNode", "createDate"); case "NAME": return GetDatabaseFieldNameForOrderBy("umbracoNode", "text"); + case "PUBLISHED": + return GetDatabaseFieldNameForOrderBy("cmsDocument", "published"); case "OWNER": //TODO: This isn't going to work very nicely because it's going to order by ID, not by letter return GetDatabaseFieldNameForOrderBy("umbracoNode", "nodeUser"); diff --git a/src/Umbraco.Core/Persistence/Repositories/XsltFileRepository.cs b/src/Umbraco.Core/Persistence/Repositories/XsltFileRepository.cs new file mode 100644 index 0000000000..461efce6c3 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/XsltFileRepository.cs @@ -0,0 +1,135 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Umbraco.Core.IO; +using Umbraco.Core.Models; +using Umbraco.Core.Persistence.UnitOfWork; + +namespace Umbraco.Core.Persistence.Repositories +{ + /// + /// Represents the XsltFile Repository + /// + internal class XsltFileRepository : FileRepository, IXsltFileRepository + { + public XsltFileRepository(IUnitOfWork work, IFileSystem fileSystem) + : base(work, fileSystem) + { + } + + public override XsltFile Get(string id) + { + var path = FileSystem.GetRelativePath(id); + + path = path.EnsureEndsWith(".xslt"); + + if (FileSystem.FileExists(path) == false) + return null; + + var created = FileSystem.GetCreated(path).UtcDateTime; + var updated = FileSystem.GetLastModified(path).UtcDateTime; + + var xsltFile = new XsltFile(path, file => GetFileContent(file.OriginalPath)) + { + Key = path.EncodeAsGuid(), + CreateDate = created, + UpdateDate = updated, + Id = path.GetHashCode(), + VirtualPath = FileSystem.GetUrl(path) + }; + + //on initial construction we don't want to have dirty properties tracked + // http://issues.umbraco.org/issue/U4-1946 + xsltFile.ResetDirtyProperties(false); + + return xsltFile; + } + + public override void AddOrUpdate(XsltFile entity) + { + base.AddOrUpdate(entity); + + // ensure that from now on, content is lazy-loaded + if (entity.GetFileContent == null) + entity.GetFileContent = file => GetFileContent(file.OriginalPath); + } + + public override IEnumerable GetAll(params string[] ids) + { + ids = ids + .Select(x => x.EnsureEndsWith(".xslt")) + .Distinct() + .ToArray(); + + if (ids.Any()) + { + foreach (var id in ids) + { + yield return Get(id); + } + } + else + { + var files = FindAllFiles("", "*.xslt"); + foreach (var file in files) + { + yield return Get(file); + } + } + } + + /// + /// Gets a list of all that exist at the relative path specified. + /// + /// + /// If null or not specified, will return the XSLT files at the root path relative to the IFileSystem + /// + /// + public IEnumerable GetXsltFilesAtPath(string rootPath = null) + { + return FileSystem.GetFiles(rootPath ?? string.Empty, "*.xslt").Select(Get); + } + + private static readonly List ValidExtensions = new List { "xslt" }; + + public bool ValidateXsltFile(XsltFile xsltFile) + { + // get full path + string fullPath; + try + { + // may throw for security reasons + fullPath = FileSystem.GetFullPath(xsltFile.Path); + } + catch + { + return false; + } + + // validate path and extension + var validDir = SystemDirectories.Xslt; + var isValidPath = IOHelper.VerifyEditPath(fullPath, validDir); + var isValidExtension = IOHelper.VerifyFileExtension(xsltFile.Path, ValidExtensions); + return isValidPath && isValidExtension; + } + + public Stream GetFileContentStream(string filepath) + { + if (FileSystem.FileExists(filepath) == false) return null; + + try + { + return FileSystem.OpenFile(filepath); + } + catch + { + return null; // deal with race conds + } + } + + public void SetFileContent(string filepath, Stream content) + { + FileSystem.AddFile(filepath, content, true); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs index 90e01b88fb..80559f9ce4 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs @@ -68,6 +68,7 @@ namespace Umbraco.Core.Persistence.SqlSyntax string Format(ForeignKeyDefinition foreignKey); string FormatColumnRename(string tableName, string oldName, string newName); string FormatTableRename(string oldName, string newName); + Sql SelectTop(Sql sql, int top); bool SupportsClustered(); bool SupportsIdentityInsert(); bool? SupportsCaseInsensitiveQueries(Database db); diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/MicrosoftSqlSyntaxProviderBase.cs b/src/Umbraco.Core/Persistence/SqlSyntax/MicrosoftSqlSyntaxProviderBase.cs index a6d1d690ba..449f5fb3b1 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/MicrosoftSqlSyntaxProviderBase.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/MicrosoftSqlSyntaxProviderBase.cs @@ -29,7 +29,11 @@ namespace Umbraco.Core.Persistence.SqlSyntax public override string GetQuotedTableName(string tableName) { - return string.Format("[{0}]", tableName); + if (tableName.Contains(".") == false) + return string.Format("[{0}]", tableName); + + var tableNameParts = tableName.Split(new[] { '.' }, 2); + return string.Format("[{0}].[{1}]", tableNameParts[0], tableNameParts[1]); } public override string GetQuotedColumnName(string columnName) diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs index 4d2200b44c..adedbf2680 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs @@ -179,6 +179,11 @@ ORDER BY TABLE_NAME, INDEX_NAME", return result > 0; } + public override Sql SelectTop(Sql sql, int top) + { + return new Sql(sql.SqlContext, string.Concat(sql.SQL, " LIMIT ", top), sql.Arguments); + } + public override bool SupportsClustered() { return true; diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs index b169943b80..84a4dfbd6d 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs @@ -14,6 +14,11 @@ namespace Umbraco.Core.Persistence.SqlSyntax [SqlSyntaxProvider(Constants.DbProviderNames.SqlCe)] public class SqlCeSyntaxProvider : MicrosoftSqlSyntaxProviderBase { + public override Sql SelectTop(Sql sql, int top) + { + return new Sql(sql.SqlContext, sql.SQL.Insert(sql.SQL.IndexOf(' '), " TOP " + top), sql.Arguments); + } + public override bool SupportsClustered() { return false; diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs index 61479627fe..51f19fe57d 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs @@ -210,6 +210,11 @@ order by T.name, I.name"); return column.IsIdentity ? GetIdentityString(column) : string.Empty; } + public override Sql SelectTop(Sql sql, int top) + { + return new Sql(sql.SqlContext, sql.SQL.Insert(sql.SQL.IndexOf(' '), " TOP " + top), sql.Arguments); + } + private static string GetIdentityString(ColumnDefinition column) { return "IDENTITY(1,1)"; diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerVersionName.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerVersionName.cs index 37870a9536..2f50f39435 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerVersionName.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerVersionName.cs @@ -3,6 +3,9 @@ /// /// Represents the version name of SQL server (i.e. the year 2008, 2005, etc...) /// + /// + /// see: https://support.microsoft.com/en-us/kb/321185 + /// internal enum SqlServerVersionName { Invalid = -1, @@ -11,6 +14,8 @@ V2005 = 2, V2008 = 3, V2012 = 4, - Other = 5 + V2014 = 5, + V2016 = 6, + Other = 100 } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs index e194c7c79c..b3834fc819 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs @@ -128,48 +128,48 @@ namespace Umbraco.Core.Persistence.SqlSyntax public virtual string GetStringColumnEqualComparison(string column, int paramIndex, TextColumnType columnType) { //use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. - return string.Format("upper({0}) = upper(@{1})", column, paramIndex); + return $"upper({column}) = upper(@{paramIndex})"; } public virtual string GetStringColumnWildcardComparison(string column, int paramIndex, TextColumnType columnType) { //use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. - return string.Format("upper({0}) LIKE upper(@{1})", column, paramIndex); + return $"upper({column}) LIKE upper(@{paramIndex})"; } [Obsolete("Use the overload with the parameter index instead")] public virtual string GetStringColumnEqualComparison(string column, string value, TextColumnType columnType) { //use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. - return string.Format("upper({0}) = '{1}'", column, value.ToUpper()); + return $"upper({column}) = '{value.ToUpper()}'"; } [Obsolete("Use the overload with the parameter index instead")] public virtual string GetStringColumnStartsWithComparison(string column, string value, TextColumnType columnType) { //use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. - return string.Format("upper({0}) LIKE '{1}%'", column, value.ToUpper()); + return $"upper({column}) LIKE '{value.ToUpper()}%'"; } [Obsolete("Use the overload with the parameter index instead")] public virtual string GetStringColumnEndsWithComparison(string column, string value, TextColumnType columnType) { //use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. - return string.Format("upper({0}) LIKE '%{1}'", column, value.ToUpper()); + return $"upper({column}) LIKE '%{value.ToUpper()}'"; } [Obsolete("Use the overload with the parameter index instead")] public virtual string GetStringColumnContainsComparison(string column, string value, TextColumnType columnType) { //use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. - return string.Format("upper({0}) LIKE '%{1}%'", column, value.ToUpper()); + return $"upper({column}) LIKE '%{value.ToUpper()}%'"; } [Obsolete("Use the overload with the parameter index instead")] public virtual string GetStringColumnWildcardComparison(string column, string value, TextColumnType columnType) { //use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. - return string.Format("upper({0}) LIKE '{1}'", column, value.ToUpper()); + return $"upper({column}) LIKE '{value.ToUpper()}'"; } public virtual string GetConcat(params string[] args) @@ -179,22 +179,22 @@ namespace Umbraco.Core.Persistence.SqlSyntax public virtual string GetQuotedTableName(string tableName) { - return string.Format("\"{0}\"", tableName); + return $"\"{tableName}\""; } public virtual string GetQuotedColumnName(string columnName) { - return string.Format("\"{0}\"", columnName); + return $"\"{columnName}\""; } public virtual string GetQuotedName(string name) { - return string.Format("\"{0}\"", name); + return $"\"{name}\""; } public virtual string GetQuotedValue(string value) { - return string.Format("'{0}'", value); + return $"'{value}'"; } public virtual string GetIndexType(IndexTypes indexTypes) @@ -299,13 +299,13 @@ namespace Umbraco.Core.Persistence.SqlSyntax public virtual string Format(IndexDefinition index) { - string name = string.IsNullOrEmpty(index.Name) - ? string.Format("IX_{0}_{1}", index.TableName, index.ColumnName) - : index.Name; + var name = string.IsNullOrEmpty(index.Name) + ? $"IX_{index.TableName}_{index.ColumnName}" + : index.Name; - string columns = index.Columns.Any() - ? string.Join(",", index.Columns.Select(x => GetQuotedColumnName(x.Name))) - : GetQuotedColumnName(index.ColumnName); + var columns = index.Columns.Any() + ? string.Join(",", index.Columns.Select(x => GetQuotedColumnName(x.Name))) + : GetQuotedColumnName(index.ColumnName); return string.Format(CreateIndex, GetIndexType(index.IndexType), " ", GetQuotedName(name), GetQuotedTableName(index.TableName), columns); @@ -318,9 +318,9 @@ namespace Umbraco.Core.Persistence.SqlSyntax public virtual string Format(ForeignKeyDefinition foreignKey) { - string constraintName = string.IsNullOrEmpty(foreignKey.Name) - ? string.Format("FK_{0}_{1}_{2}", foreignKey.ForeignTable, foreignKey.PrimaryTable, foreignKey.PrimaryColumns.First()) - : foreignKey.Name; + var constraintName = string.IsNullOrEmpty(foreignKey.Name) + ? $"FK_{foreignKey.ForeignTable}_{foreignKey.PrimaryTable}_{foreignKey.PrimaryColumns.First()}" + : foreignKey.Name; return string.Format(CreateForeignKeyConstraint, GetQuotedTableName(foreignKey.ForeignTable), @@ -344,16 +344,9 @@ namespace Umbraco.Core.Persistence.SqlSyntax public virtual string Format(ColumnDefinition column) { - var clauses = new List(); - - foreach (var action in ClauseOrder) - { - string clause = action(column); - if (!string.IsNullOrEmpty(clause)) - clauses.Add(clause); - } - - return string.Join(" ", clauses.ToArray()); + return string.Join(" ", ClauseOrder + .Select(action => action(column)) + .Where(clause => string.IsNullOrEmpty(clause) == false)); } public virtual string FormatPrimaryKey(TableDefinition table) @@ -362,17 +355,17 @@ namespace Umbraco.Core.Persistence.SqlSyntax if (columnDefinition == null) return string.Empty; - string constraintName = string.IsNullOrEmpty(columnDefinition.PrimaryKeyName) - ? string.Format("PK_{0}", table.Name) - : columnDefinition.PrimaryKeyName; + var constraintName = string.IsNullOrEmpty(columnDefinition.PrimaryKeyName) + ? $"PK_{table.Name}" + : columnDefinition.PrimaryKeyName; - string columns = string.IsNullOrEmpty(columnDefinition.PrimaryKeyColumns) - ? GetQuotedColumnName(columnDefinition.Name) - : string.Join(", ", columnDefinition.PrimaryKeyColumns - .Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries) - .Select(GetQuotedColumnName)); + var columns = string.IsNullOrEmpty(columnDefinition.PrimaryKeyColumns) + ? GetQuotedColumnName(columnDefinition.Name) + : string.Join(", ", columnDefinition.PrimaryKeyColumns + .Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries) + .Select(GetQuotedColumnName)); - string primaryKeyPart = string.Concat("PRIMARY KEY", columnDefinition.IsIndexed ? " CLUSTERED" : " NONCLUSTERED"); + var primaryKeyPart = string.Concat("PRIMARY KEY", columnDefinition.IsIndexed ? " CLUSTERED" : " NONCLUSTERED"); return string.Format(CreateConstraint, GetQuotedTableName(table.Name), @@ -396,7 +389,7 @@ namespace Umbraco.Core.Persistence.SqlSyntax protected virtual string FormatCascade(string onWhat, Rule rule) { - string action = "NO ACTION"; + var action = "NO ACTION"; switch (rule) { case Rule.None: @@ -412,7 +405,7 @@ namespace Umbraco.Core.Persistence.SqlSyntax break; } - return string.Format(" ON {0} {1}", onWhat, action); + return $" ON {onWhat} {action}"; } protected virtual string FormatString(ColumnDefinition column) @@ -429,15 +422,13 @@ namespace Umbraco.Core.Persistence.SqlSyntax { if (column.Size != default(int)) { - return string.Format("{0}({1})", - GetSpecialDbType(column.DbType), - column.Size); + return $"{GetSpecialDbType(column.DbType)}({column.Size})"; } return GetSpecialDbType(column.DbType); } - Type type = column.Type.HasValue + var type = column.Type.HasValue ? DbTypeMap.ColumnDbTypeMap.First(x => x.Value == column.Type.Value).Key : column.PropertyType; @@ -454,10 +445,10 @@ namespace Umbraco.Core.Persistence.SqlSyntax return string.Format(DecimalColumnDefinitionFormat, precision, scale); } - string definition = DbTypeMap.ColumnTypeMap.First(x => x.Key == type).Value; - string dbTypeDefinition = column.Size != default(int) - ? string.Format("{0}({1})", definition, column.Size) - : definition; + var definition = DbTypeMap.ColumnTypeMap.First(x => x.Key == type).Value; + var dbTypeDefinition = column.Size != default(int) + ? $"{definition}({column.Size})" + : definition; //NOTE Percision is left out return dbTypeDefinition; } @@ -472,10 +463,8 @@ namespace Umbraco.Core.Persistence.SqlSyntax if (string.IsNullOrEmpty(column.ConstraintName) && column.DefaultValue == null) return string.Empty; - return string.Format("CONSTRAINT {0}", - string.IsNullOrEmpty(column.ConstraintName) - ? GetQuotedName(string.Format("DF_{0}_{1}", column.TableName, column.Name)) - : column.ConstraintName); + return + $"CONSTRAINT {(string.IsNullOrEmpty(column.ConstraintName) ? GetQuotedName($"DF_{column.TableName}_{column.Name}") : column.ConstraintName)}"; } protected virtual string FormatDefaultValue(ColumnDefinition column) @@ -490,11 +479,8 @@ namespace Umbraco.Core.Persistence.SqlSyntax // see if this is for a system method if (column.DefaultValue is SystemMethods) { - string method = FormatSystemMethods((SystemMethods)column.DefaultValue); - if (string.IsNullOrEmpty(method)) - return string.Empty; - - return string.Format(DefaultValueFormat, method); + var method = FormatSystemMethods((SystemMethods)column.DefaultValue); + return string.IsNullOrEmpty(method) ? string.Empty : string.Format(DefaultValueFormat, method); } return string.Format(DefaultValueFormat, GetQuotedValue(column.DefaultValue.ToString())); @@ -509,6 +495,8 @@ namespace Umbraco.Core.Persistence.SqlSyntax protected abstract string FormatIdentity(ColumnDefinition column); + public abstract Sql SelectTop(Sql sql, int top); + public virtual string DeleteDefaultConstraint { get @@ -517,34 +505,34 @@ namespace Umbraco.Core.Persistence.SqlSyntax } } - public virtual string CreateTable { get { return "CREATE TABLE {0} ({1})"; } } - public virtual string DropTable { get { return "DROP TABLE {0}"; } } + public virtual string CreateTable => "CREATE TABLE {0} ({1})"; + public virtual string DropTable => "DROP TABLE {0}"; - public virtual string AddColumn { get { return "ALTER TABLE {0} ADD COLUMN {1}"; } } - public virtual string DropColumn { get { return "ALTER TABLE {0} DROP COLUMN {1}"; } } - public virtual string AlterColumn { get { return "ALTER TABLE {0} ALTER COLUMN {1}"; } } - public virtual string RenameColumn { get { return "ALTER TABLE {0} RENAME COLUMN {1} TO {2}"; } } + public virtual string AddColumn => "ALTER TABLE {0} ADD COLUMN {1}"; + public virtual string DropColumn => "ALTER TABLE {0} DROP COLUMN {1}"; + public virtual string AlterColumn => "ALTER TABLE {0} ALTER COLUMN {1}"; + public virtual string RenameColumn => "ALTER TABLE {0} RENAME COLUMN {1} TO {2}"; - public virtual string RenameTable { get { return "RENAME TABLE {0} TO {1}"; } } + public virtual string RenameTable => "RENAME TABLE {0} TO {1}"; - public virtual string CreateSchema { get { return "CREATE SCHEMA {0}"; } } - public virtual string AlterSchema { get { return "ALTER SCHEMA {0} TRANSFER {1}.{2}"; } } - public virtual string DropSchema { get { return "DROP SCHEMA {0}"; } } + public virtual string CreateSchema => "CREATE SCHEMA {0}"; + public virtual string AlterSchema => "ALTER SCHEMA {0} TRANSFER {1}.{2}"; + public virtual string DropSchema => "DROP SCHEMA {0}"; - public virtual string CreateIndex { get { return "CREATE {0}{1}INDEX {2} ON {3} ({4})"; } } - public virtual string DropIndex { get { return "DROP INDEX {0}"; } } + public virtual string CreateIndex => "CREATE {0}{1}INDEX {2} ON {3} ({4})"; + public virtual string DropIndex => "DROP INDEX {0}"; - public virtual string InsertData { get { return "INSERT INTO {0} ({1}) VALUES ({2})"; } } - public virtual string UpdateData { get { return "UPDATE {0} SET {1} WHERE {2}"; } } - public virtual string DeleteData { get { return "DELETE FROM {0} WHERE {1}"; } } - public virtual string TruncateTable { get { return "TRUNCATE TABLE {0}"; } } + public virtual string InsertData => "INSERT INTO {0} ({1}) VALUES ({2})"; + public virtual string UpdateData => "UPDATE {0} SET {1} WHERE {2}"; + public virtual string DeleteData => "DELETE FROM {0} WHERE {1}"; + public virtual string TruncateTable => "TRUNCATE TABLE {0}"; - public virtual string CreateConstraint { get { return "ALTER TABLE {0} ADD CONSTRAINT {1} {2} ({3})"; } } - public virtual string DeleteConstraint { get { return "ALTER TABLE {0} DROP CONSTRAINT {1}"; } } - public virtual string CreateForeignKeyConstraint { get { return "ALTER TABLE {0} ADD CONSTRAINT {1} FOREIGN KEY ({2}) REFERENCES {3} ({4}){5}{6}"; } } + public virtual string CreateConstraint => "ALTER TABLE {0} ADD CONSTRAINT {1} {2} ({3})"; + public virtual string DeleteConstraint => "ALTER TABLE {0} DROP CONSTRAINT {1}"; + public virtual string CreateForeignKeyConstraint => "ALTER TABLE {0} ADD CONSTRAINT {1} FOREIGN KEY ({2}) REFERENCES {3} ({4}){5}{6}"; - public virtual string ConvertIntegerToOrderableString { get { return "REPLACE(STR({0}, 8), SPACE(1), '0')"; } } - public virtual string ConvertDateToOrderableString { get { return "CONVERT(nvarchar, {0}, 102)"; } } - public virtual string ConvertDecimalToOrderableString { get { return "REPLACE(STR({0}, 20, 9), SPACE(1), '0')"; } } + public virtual string ConvertIntegerToOrderableString => "REPLACE(STR({0}, 8), SPACE(1), '0')"; + public virtual string ConvertDateToOrderableString => "CONVERT(nvarchar, {0}, 102)"; + public virtual string ConvertDecimalToOrderableString => "REPLACE(STR({0}, 20, 9), SPACE(1), '0')"; } } \ No newline at end of file diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/MultipleTextStringValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/MultipleTextStringValueConverter.cs index 9ee38e7830..5d1949f5a2 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/MultipleTextStringValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/MultipleTextStringValueConverter.cs @@ -34,7 +34,7 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters // // - var sourceString = source.ToString(); + var sourceString = source != null ? source.ToString() : null; if (string.IsNullOrWhiteSpace(sourceString)) return Enumerable.Empty(); //SD: I have no idea why this logic is here, I'm pretty sure we've never saved the multiple txt string diff --git a/src/Umbraco.Core/Security/ActiveDirectoryBackOfficeUserPasswordChecker.cs b/src/Umbraco.Core/Security/ActiveDirectoryBackOfficeUserPasswordChecker.cs new file mode 100644 index 0000000000..819fa87a56 --- /dev/null +++ b/src/Umbraco.Core/Security/ActiveDirectoryBackOfficeUserPasswordChecker.cs @@ -0,0 +1,31 @@ +using System.Configuration; +using System.DirectoryServices.AccountManagement; +using System.Threading.Tasks; +using Umbraco.Core.Models.Identity; + +namespace Umbraco.Core.Security +{ + public class ActiveDirectoryBackOfficeUserPasswordChecker : IBackOfficeUserPasswordChecker + { + public virtual string ActiveDirectoryDomain { + get { + return ConfigurationManager.AppSettings["ActiveDirectoryDomain"]; + } + } + + public Task CheckPasswordAsync(BackOfficeIdentityUser user, string password) + { + bool isValid; + using (var pc = new PrincipalContext(ContextType.Domain, ActiveDirectoryDomain)) + { + isValid = pc.ValidateCredentials(user.UserName, password); + } + + var result = isValid + ? BackOfficeUserPasswordCheckerResult.ValidCredentials + : BackOfficeUserPasswordCheckerResult.InvalidCredentials; + + return Task.FromResult(result); + } + } +} diff --git a/src/Umbraco.Core/Security/AuthenticationExtensions.cs b/src/Umbraco.Core/Security/AuthenticationExtensions.cs index 99010c85ee..6475a6cf0c 100644 --- a/src/Umbraco.Core/Security/AuthenticationExtensions.cs +++ b/src/Umbraco.Core/Security/AuthenticationExtensions.cs @@ -114,8 +114,8 @@ namespace Umbraco.Core.Security //Otherwise convert to a UmbracoBackOfficeIdentity if it's auth'd and has the back office session var claimsIdentity = http.User.Identity as ClaimsIdentity; - if (claimsIdentity != null && claimsIdentity.IsAuthenticated) - { + if (claimsIdentity != null && claimsIdentity.IsAuthenticated && claimsIdentity.HasClaim(x => x.Type == Constants.Security.SessionIdClaimType)) + { try { return UmbracoBackOfficeIdentity.FromClaimsIdentity(claimsIdentity); diff --git a/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs b/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs index 9c46ae69f4..7c2373a000 100644 --- a/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs +++ b/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs @@ -7,7 +7,8 @@ using Umbraco.Core.Models.Identity; namespace Umbraco.Core.Security { - public class BackOfficeClaimsIdentityFactory : ClaimsIdentityFactory + public class BackOfficeClaimsIdentityFactory : ClaimsIdentityFactory + where T: BackOfficeIdentityUser { public BackOfficeClaimsIdentityFactory() { @@ -20,7 +21,7 @@ namespace Umbraco.Core.Security /// /// /// - public override async Task CreateAsync(UserManager manager, BackOfficeIdentityUser user, string authenticationType) + public override async Task CreateAsync(UserManager manager, T user, string authenticationType) { var baseIdentity = await base.CreateAsync(manager, user, authenticationType); @@ -42,4 +43,9 @@ namespace Umbraco.Core.Security return umbracoIdentity; } } + + public class BackOfficeClaimsIdentityFactory : BackOfficeClaimsIdentityFactory + { + + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Security/BackOfficeSignInManager.cs b/src/Umbraco.Core/Security/BackOfficeSignInManager.cs index 1707813433..7c65b43291 100644 --- a/src/Umbraco.Core/Security/BackOfficeSignInManager.cs +++ b/src/Umbraco.Core/Security/BackOfficeSignInManager.cs @@ -17,7 +17,7 @@ namespace Umbraco.Core.Security private readonly ILogger _logger; private readonly IOwinRequest _request; - public BackOfficeSignInManager(BackOfficeUserManager userManager, IAuthenticationManager authenticationManager, ILogger logger, IOwinRequest request) + public BackOfficeSignInManager(UserManager userManager, IAuthenticationManager authenticationManager, ILogger logger, IOwinRequest request) : base(userManager, authenticationManager) { if (logger == null) throw new ArgumentNullException("logger"); @@ -29,13 +29,13 @@ namespace Umbraco.Core.Security public override Task CreateUserIdentityAsync(BackOfficeIdentityUser user) { - return user.GenerateUserIdentityAsync((BackOfficeUserManager)UserManager); + return user.GenerateUserIdentityAsync((BackOfficeUserManager)UserManager); } public static BackOfficeSignInManager Create(IdentityFactoryOptions options, IOwinContext context, ILogger logger) { return new BackOfficeSignInManager( - context.GetUserManager(), + context.GetBackOfficeUserManager(), context.Authentication, logger, context.Request); diff --git a/src/Umbraco.Core/Security/BackOfficeUserManager.cs b/src/Umbraco.Core/Security/BackOfficeUserManager.cs index b786a7c93f..0397057d6d 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserManager.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserManager.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using System.Web.Security; using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.Owin; +using Microsoft.Owin.Security.DataProtection; using Umbraco.Core.Models.Identity; using Umbraco.Core.Services; @@ -15,6 +16,8 @@ namespace Umbraco.Core.Security /// public class BackOfficeUserManager : BackOfficeUserManager { + public const string OwinMarkerKey = "Umbraco.Web.Security.Identity.BackOfficeUserManagerMarker"; + public BackOfficeUserManager(IUserStore store) : base(store) { @@ -26,9 +29,8 @@ namespace Umbraco.Core.Security MembershipProviderBase membershipProvider) : base(store) { - if (options == null) throw new ArgumentNullException("options"); - var manager = new BackOfficeUserManager(store); - InitUserManager(manager, membershipProvider, options); + if (options == null) throw new ArgumentNullException("options");; + InitUserManager(this, membershipProvider, options); } #region Static Create methods @@ -73,7 +75,7 @@ namespace Umbraco.Core.Security { var manager = new BackOfficeUserManager(customUserStore, options, membershipProvider); return manager; - } + } #endregion /// @@ -83,65 +85,15 @@ namespace Umbraco.Core.Security /// /// /// - protected void InitUserManager(BackOfficeUserManager manager, MembershipProviderBase membershipProvider, IdentityFactoryOptions options) + protected void InitUserManager( + BackOfficeUserManager manager, + MembershipProviderBase membershipProvider, + IdentityFactoryOptions options) { - // Configure validation logic for usernames - manager.UserValidator = new UserValidator(manager) - { - AllowOnlyAlphanumericUserNames = false, - RequireUniqueEmail = true - }; - - // Configure validation logic for passwords - manager.PasswordValidator = new PasswordValidator - { - RequiredLength = membershipProvider.MinRequiredPasswordLength, - RequireNonLetterOrDigit = membershipProvider.MinRequiredNonAlphanumericCharacters > 0, - RequireDigit = false, - RequireLowercase = false, - RequireUppercase = false - //TODO: Do we support the old regex match thing that membership providers used? - }; - - //use a custom hasher based on our membership provider - manager.PasswordHasher = new MembershipPasswordHasher(membershipProvider); - - var dataProtectionProvider = options.DataProtectionProvider; - if (dataProtectionProvider != null) - { - manager.UserTokenProvider = new DataProtectorTokenProvider(dataProtectionProvider.Create("ASP.NET Identity")); - } - - manager.UserLockoutEnabledByDefault = true; - manager.MaxFailedAccessAttemptsBeforeLockout = membershipProvider.MaxInvalidPasswordAttempts; - //NOTE: This just needs to be in the future, we currently don't support a lockout timespan, it's either they are locked - // or they are not locked, but this determines what is set on the account lockout date which corresponds to whether they are - // locked out or not. - manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromDays(30); - - //custom identity factory for creating the identity object for which we auth against in the back office - manager.ClaimsIdentityFactory = new BackOfficeClaimsIdentityFactory(); - - manager.EmailService = new EmailService(); - - //NOTE: Not implementing these, if people need custom 2 factor auth, they'll need to implement their own UserStore to suport it - - //// Register two factor authentication providers. This application uses Phone and Emails as a step of receiving a code for verifying the user - //// You can write your own provider and plug in here. - //manager.RegisterTwoFactorProvider("PhoneCode", new PhoneNumberTokenProvider - //{ - // MessageFormat = "Your security code is: {0}" - //}); - //manager.RegisterTwoFactorProvider("EmailCode", new EmailTokenProvider - //{ - // Subject = "Security Code", - // BodyFormat = "Your security code is: {0}" - //}); - - //manager.SmsService = new SmsService(); + //NOTE: This method is mostly here for backwards compat + base.InitUserManager(manager, membershipProvider, options.DataProtectionProvider); } - } /// @@ -184,6 +136,73 @@ namespace Umbraco.Core.Security } #endregion + /// + /// Initializes the user manager with the correct options + /// + /// + /// + /// + /// + protected void InitUserManager( + BackOfficeUserManager manager, + MembershipProviderBase membershipProvider, + IDataProtectionProvider dataProtectionProvider) + { + // Configure validation logic for usernames + manager.UserValidator = new UserValidator(manager) + { + AllowOnlyAlphanumericUserNames = false, + RequireUniqueEmail = true + }; + + // Configure validation logic for passwords + manager.PasswordValidator = new PasswordValidator + { + RequiredLength = membershipProvider.MinRequiredPasswordLength, + RequireNonLetterOrDigit = membershipProvider.MinRequiredNonAlphanumericCharacters > 0, + RequireDigit = false, + RequireLowercase = false, + RequireUppercase = false + //TODO: Do we support the old regex match thing that membership providers used? + }; + + //use a custom hasher based on our membership provider + manager.PasswordHasher = new MembershipPasswordHasher(membershipProvider); + + if (dataProtectionProvider != null) + { + manager.UserTokenProvider = new DataProtectorTokenProvider(dataProtectionProvider.Create("ASP.NET Identity")); + } + + manager.UserLockoutEnabledByDefault = true; + manager.MaxFailedAccessAttemptsBeforeLockout = membershipProvider.MaxInvalidPasswordAttempts; + //NOTE: This just needs to be in the future, we currently don't support a lockout timespan, it's either they are locked + // or they are not locked, but this determines what is set on the account lockout date which corresponds to whether they are + // locked out or not. + manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromDays(30); + + //custom identity factory for creating the identity object for which we auth against in the back office + manager.ClaimsIdentityFactory = new BackOfficeClaimsIdentityFactory(); + + manager.EmailService = new EmailService(); + + //NOTE: Not implementing these, if people need custom 2 factor auth, they'll need to implement their own UserStore to suport it + + //// Register two factor authentication providers. This application uses Phone and Emails as a step of receiving a code for verifying the user + //// You can write your own provider and plug in here. + //manager.RegisterTwoFactorProvider("PhoneCode", new PhoneNumberTokenProvider + //{ + // MessageFormat = "Your security code is: {0}" + //}); + //manager.RegisterTwoFactorProvider("EmailCode", new EmailTokenProvider + //{ + // Subject = "Security Code", + // BodyFormat = "Your security code is: {0}" + //}); + + //manager.SmsService = new SmsService(); + } + /// /// Logic used to validate a username and password /// diff --git a/src/Umbraco.Core/Security/BackOfficeUserManagerMarker.cs b/src/Umbraco.Core/Security/BackOfficeUserManagerMarker.cs new file mode 100644 index 0000000000..9dd5afd9da --- /dev/null +++ b/src/Umbraco.Core/Security/BackOfficeUserManagerMarker.cs @@ -0,0 +1,26 @@ +using System; +using Microsoft.AspNet.Identity.Owin; +using Microsoft.Owin; +using Umbraco.Core.Models.Identity; + +namespace Umbraco.Core.Security +{ + /// + /// This class is only here due to the fact that IOwinContext Get / Set only work in generics, if they worked + /// with regular 'object' then we wouldn't have to use this work around but because of that we have to use this + /// class to resolve the 'real' type of the registered user manager + /// + /// + /// + internal class BackOfficeUserManagerMarker : IBackOfficeUserManagerMarker + where TManager : BackOfficeUserManager + where TUser : BackOfficeIdentityUser + { + public BackOfficeUserManager GetManager(IOwinContext owin) + { + var mgr = owin.Get() as BackOfficeUserManager; + if (mgr == null) throw new InvalidOperationException("Could not cast the registered back office user of type " + typeof(TManager) + " to " + typeof(BackOfficeUserManager)); + return mgr; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Security/IBackOfficeUserManagerMarker.cs b/src/Umbraco.Core/Security/IBackOfficeUserManagerMarker.cs new file mode 100644 index 0000000000..0dd98cd57a --- /dev/null +++ b/src/Umbraco.Core/Security/IBackOfficeUserManagerMarker.cs @@ -0,0 +1,15 @@ +using Microsoft.Owin; +using Umbraco.Core.Models.Identity; + +namespace Umbraco.Core.Security +{ + /// + /// This interface is only here due to the fact that IOwinContext Get / Set only work in generics, if they worked + /// with regular 'object' then we wouldn't have to use this work around but because of that we have to use this + /// class to resolve the 'real' type of the registered user manager + /// + internal interface IBackOfficeUserManagerMarker + { + BackOfficeUserManager GetManager(IOwinContext owin); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Security/OwinExtensions.cs b/src/Umbraco.Core/Security/OwinExtensions.cs new file mode 100644 index 0000000000..251f008a8c --- /dev/null +++ b/src/Umbraco.Core/Security/OwinExtensions.cs @@ -0,0 +1,47 @@ +using System; +using Microsoft.AspNet.Identity.Owin; +using Microsoft.Owin; +using Umbraco.Core.Models.Identity; + +namespace Umbraco.Core.Security +{ + public static class OwinExtensions + { + /// + /// Gets the back office sign in manager out of OWIN + /// + /// + /// + public static BackOfficeSignInManager GetBackOfficeSignInManager(this IOwinContext owinContext) + { + var mgr = owinContext.Get(); + if (mgr == null) + { + throw new NullReferenceException("Could not resolve an instance of " + typeof(BackOfficeSignInManager) + " from the " + typeof(IOwinContext)); + } + return mgr; + } + + /// + /// Gets the back office user manager out of OWIN + /// + /// + /// + /// + /// This is required because to extract the user manager we need to user a custom service since owin only deals in generics and + /// developers could register their own user manager types + /// + public static BackOfficeUserManager GetBackOfficeUserManager(this IOwinContext owinContext) + { + var marker = owinContext.Get(BackOfficeUserManager.OwinMarkerKey); + if (marker == null) throw new NullReferenceException("No " + typeof(IBackOfficeUserManagerMarker) + " has been registered with Owin which means that no Umbraco back office user manager has been registered"); + + var mgr = marker.GetManager(owinContext); + if (mgr == null) + { + throw new NullReferenceException("Could not resolve an instance of " + typeof(BackOfficeUserManager)); + } + return mgr; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 8ceea3de36..ef52635030 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -20,15 +20,18 @@ namespace Umbraco.Core.Services /// public class ContentService : RepositoryService, IContentService, IContentServiceOperations { + private readonly MediaFileSystem _mediaFileSystem; #region Constructors public ContentService( IDatabaseUnitOfWorkProvider provider, ILogger logger, - IEventMessagesFactory eventMessagesFactory) + IEventMessagesFactory eventMessagesFactory, + MediaFileSystem mediaFileSystem) : base(provider, logger, eventMessagesFactory) { + _mediaFileSystem = mediaFileSystem; } #endregion @@ -366,7 +369,14 @@ namespace Umbraco.Core.Services var repository = uow.CreateRepository(); var items = repository.GetAll(idsA); uow.Complete(); - return items; + + var index = items.ToDictionary(x => x.Id, x => x); + + return idsA.Select(x => + { + IContent c; + return index.TryGetValue(x, out c) ? c : null; + }).WhereNotNull(); } } @@ -472,6 +482,23 @@ namespace Umbraco.Core.Services } } + /// + /// Gets a list of all version Ids for the given content item ordered so latest is first + /// + /// + /// The maximum number of rows to return + /// + public IEnumerable GetVersionIds(int id, int maxRows) + { + using (var uow = UowProvider.CreateUnitOfWork()) + { + var repository = uow.CreateRepository(); + var versions = repository.GetVersionIds(id, maxRows); + uow.Complete(); + return versions; + } + } + /// /// Gets a collection of objects, which are ancestors of the current content. /// @@ -1317,7 +1344,7 @@ namespace Umbraco.Core.Services var args = new DeleteEventArgs(c, false); // raise event & get flagged files Deleted.RaiseEvent(args, this); - IOHelper.DeleteFiles(args.MediaFilesToDelete, // remove flagged files + _mediaFileSystem.DeleteFiles(args.MediaFilesToDelete, // remove flagged files (file, e) => Logger.Error("An error occurred while deleting file attached to nodes: " + file, e)); } } diff --git a/src/Umbraco.Core/Services/EntityService.cs b/src/Umbraco.Core/Services/EntityService.cs index 2154241524..ca8e936c3a 100644 --- a/src/Umbraco.Core/Services/EntityService.cs +++ b/src/Umbraco.Core/Services/EntityService.cs @@ -150,7 +150,7 @@ namespace Umbraco.Core.Services case UmbracoObjectTypes.ROOT: case UmbracoObjectTypes.Unknown: default: - throw new NotSupportedException(); + throw new NotSupportedException("Unsupported object type (" + umbracoObjectType + ")."); } uow.Complete(); return guid; diff --git a/src/Umbraco.Core/Services/FileService.cs b/src/Umbraco.Core/Services/FileService.cs index 2395f719ae..dd089d1072 100644 --- a/src/Umbraco.Core/Services/FileService.cs +++ b/src/Umbraco.Core/Services/FileService.cs @@ -3,11 +3,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Linq; -using System.Runtime.Remoting.Messaging; using System.Text.RegularExpressions; -using System.Web; -using Umbraco.Core.Configuration; -using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Events; using Umbraco.Core.IO; using Umbraco.Core.Logging; @@ -139,9 +135,31 @@ namespace Umbraco.Core.Services } } + public Stream GetStylesheetFileContentStream(string filepath) + { + using (var uow = UowProvider.CreateUnitOfWork()) + { + var repository = uow.CreateRepository(); + var stream = repository.GetFileContentStream(filepath); + uow.Complete(); + return stream; + } + } + + public void SetStylesheetFileContent(string filepath, Stream content) + { + using (var uow = UowProvider.CreateUnitOfWork()) + { + var repository = uow.CreateRepository(); + repository.SetFileContent(filepath, content); + uow.Complete(); + } + } + #endregion #region Scripts + /// /// Gets a list of all objects /// @@ -260,6 +278,27 @@ namespace Umbraco.Core.Services } } + public Stream GetScriptFileContentStream(string filepath) + { + using (var uow = UowProvider.CreateUnitOfWork()) + { + var repository = uow.CreateRepository(); + var stream = repository.GetFileContentStream(filepath); + uow.Complete(); + return stream; + } + } + + public void SetScriptFileContent(string filepath, Stream content) + { + using (var uow = UowProvider.CreateUnitOfWork()) + { + var repository = uow.CreateRepository(); + repository.SetFileContent(filepath, content); + uow.Complete(); + } + } + #endregion #region Templates @@ -614,6 +653,27 @@ namespace Umbraco.Core.Services } } + public Stream GetTemplateFileContentStream(string filepath) + { + using (var uow = UowProvider.CreateUnitOfWork()) + { + var repository = uow.CreateRepository(); + var stream = repository.GetFileContentStream(filepath); + uow.Complete(); + return stream; + } + } + + public void SetTemplateFileContent(string filepath, Stream content) + { + using (var uow = UowProvider.CreateUnitOfWork()) + { + var repository = uow.CreateRepository(); + repository.SetFileContent(filepath, content); + uow.Complete(); + } + } + #endregion #region Partial Views @@ -676,6 +736,39 @@ namespace Umbraco.Core.Services } } + public IEnumerable GetPartialViewMacros(params string[] names) + { + using (var uow = _fileUowProvider.CreateUnitOfWork()) + { + var repository = uow.CreateRepository(); + var views = repository.GetAll(names).OrderBy(x => x.Name); + uow.Complete(); + return views; + } + } + + public IXsltFile GetXsltFile(string path) + { + using (var uow = _fileUowProvider.CreateUnitOfWork()) + { + var repository = uow.CreateRepository(); + var file = repository.Get(path); + uow.Complete(); + return file; + } + } + + public IEnumerable GetXsltFiles(params string[] names) + { + using (var uow = _fileUowProvider.CreateUnitOfWork()) + { + var repository = uow.CreateRepository(); + var files = repository.GetAll(names).OrderBy(x => x.Name); + uow.Complete(); + return files; + } + } + public Attempt CreatePartialView(IPartialView partialView, string snippetName = null, int userId = 0) { return CreatePartialViewMacro(partialView, PartialViewType.PartialView, snippetName, userId); @@ -842,6 +935,73 @@ namespace Umbraco.Core.Services : Attempt.Fail(); } + public Stream GetPartialViewMacroFileContentStream(string filepath) + { + using (var uow = UowProvider.CreateUnitOfWork()) + { + var repository = uow.CreatePartialViewRepository(PartialViewType.PartialViewMacro); + var stream = repository.GetFileContentStream(filepath); + uow.Complete(); + return stream; + } + } + + public void SetPartialViewMacroFileContent(string filepath, Stream content) + { + using (var uow = UowProvider.CreateUnitOfWork()) + { + var repository = uow.CreatePartialViewRepository(PartialViewType.PartialViewMacro); + repository.SetFileContent(filepath, content); + uow.Complete(); + } + } + + public Stream GetPartialViewFileContentStream(string filepath) + { + using (var uow = UowProvider.CreateUnitOfWork()) + { + var repository = uow.CreatePartialViewRepository(PartialViewType.PartialView); + var stream = repository.GetFileContentStream(filepath); + uow.Complete(); + return stream; + } + } + + public void SetPartialViewFileContent(string filepath, Stream content) + { + using (var uow = UowProvider.CreateUnitOfWork()) + { + var repository = uow.CreatePartialViewRepository(PartialViewType.PartialView); + repository.SetFileContent(filepath, content); + uow.Complete(); + } + } + + #endregion + + #region Xslt + + public Stream GetXsltFileContentStream(string filepath) + { + using (var uow = UowProvider.CreateUnitOfWork()) + { + var repository = uow.CreateRepository(); + var stream = repository.GetFileContentStream(filepath); + uow.Complete(); + return stream; + } + } + + public void SetXsltFileContent(string filepath, Stream content) + { + using (var uow = UowProvider.CreateUnitOfWork()) + { + var repository = uow.CreateRepository(); + repository.SetFileContent(filepath, content); + uow.Complete(); + } + } + #endregion private void Audit(AuditType type, string message, int userId, int objectId) diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index cc774f94a2..2f70ed43e3 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Xml; using System.Xml.Linq; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; @@ -262,6 +263,14 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects IEnumerable GetVersions(int id); + /// + /// Gets a list of all version Ids for the given content, item ordered so latest is first. + /// + /// + /// The maximum number of rows to return + /// + IEnumerable GetVersionIds(int id, int maxRows); + /// /// Gets a collection of objects, which reside at the first level / root /// diff --git a/src/Umbraco.Core/Services/IFileService.cs b/src/Umbraco.Core/Services/IFileService.cs index 3311f4a09c..a24ac11f5f 100644 --- a/src/Umbraco.Core/Services/IFileService.cs +++ b/src/Umbraco.Core/Services/IFileService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using Umbraco.Core.Models; namespace Umbraco.Core.Services @@ -14,6 +15,9 @@ namespace Umbraco.Core.Services void DeletePartialViewMacroFolder(string folderPath); IPartialView GetPartialView(string path); IPartialView GetPartialViewMacro(string path); + IEnumerable GetPartialViewMacros(params string[] names); + IXsltFile GetXsltFile(string path); + IEnumerable GetXsltFiles(params string[] names); Attempt CreatePartialView(IPartialView partialView, string snippetName = null, int userId = 0); Attempt CreatePartialViewMacro(IPartialView partialView, string snippetName = null, int userId = 0); bool DeletePartialView(string path, int userId = 0); @@ -235,5 +239,89 @@ namespace Umbraco.Core.Services /// templates in business logic. Without this, it could cause the wrong rendering engine to be used for a package. /// RenderingEngine DetermineTemplateRenderingEngine(ITemplate template); + + /// + /// Gets the content of a template as a stream. + /// + /// The filesystem path to the template. + /// The content of the template. + Stream GetTemplateFileContentStream(string filepath); + + /// + /// Sets the content of a template. + /// + /// The filesystem path to the template. + /// The content of the template. + void SetTemplateFileContent(string filepath, Stream content); + + /// + /// Gets the content of a stylesheet as a stream. + /// + /// The filesystem path to the stylesheet. + /// The content of the stylesheet. + Stream GetStylesheetFileContentStream(string filepath); + + /// + /// Sets the content of a stylesheet. + /// + /// The filesystem path to the stylesheet. + /// The content of the stylesheet. + void SetStylesheetFileContent(string filepath, Stream content); + + /// + /// Gets the content of a script file as a stream. + /// + /// The filesystem path to the script. + /// The content of the script file. + Stream GetScriptFileContentStream(string filepath); + + /// + /// Sets the content of a script file. + /// + /// The filesystem path to the script. + /// The content of the script file. + void SetScriptFileContent(string filepath, Stream content); + + /// + /// Gets the content of a XSLT file as a stream. + /// + /// The filesystem path to the XSLT file. + /// The content of the XSLT file. + Stream GetXsltFileContentStream(string filepath); + + /// + /// Sets the content of a XSLT file. + /// + /// The filesystem path to the XSLT file. + /// The content of the XSLT file. + void SetXsltFileContent(string filepath, Stream content); + + /// + /// Gets the content of a macro partial view as a stream. + /// + /// The filesystem path to the macro partial view. + /// The content of the macro partial view. + Stream GetPartialViewMacroFileContentStream(string filepath); + + /// + /// Sets the content of a macro partial view. + /// + /// The filesystem path to the macro partial view. + /// The content of the macro partial view. + void SetPartialViewMacroFileContent(string filepath, Stream content); + + /// + /// Gets the content of a partial view as a stream. + /// + /// The filesystem path to the partial view. + /// The content of the partial view. + Stream GetPartialViewFileContentStream(string filepath); + + /// + /// Sets the content of a partial view. + /// + /// The filesystem path to the partial view. + /// The content of the partial view. + void SetPartialViewFileContent(string filepath, Stream content); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/IMediaService.cs b/src/Umbraco.Core/Services/IMediaService.cs index e06cf815c1..a3b6e6a58c 100644 --- a/src/Umbraco.Core/Services/IMediaService.cs +++ b/src/Umbraco.Core/Services/IMediaService.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; using Umbraco.Core.Configuration; +using System.IO; using Umbraco.Core.Models; using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Querying; @@ -382,5 +383,33 @@ namespace Umbraco.Core.Services /// Optional id of the user creating the media item /// IMedia CreateMediaWithIdentity(string name, int parentId, string mediaTypeAlias, int userId = 0); + + /// + /// Gets the content of a media as a stream. + /// + /// The filesystem path to the media. + /// The content of the media. + Stream GetMediaFileContentStream(string filepath); + + /// + /// Sets the content of a media. + /// + /// The filesystem path to the media. + /// The content of the media. + void SetMediaFileContent(string filepath, Stream content); + + /// + /// Deletes a media file and all thumbnails. + /// + /// The filesystem path to the media. + void DeleteMediaFile(string filepath); + + /// + /// Generates thumbnails. + /// + /// The filesystem-relative path to the original image. + /// The property type. + /// This should be obsoleted, we should not generate thumbnails. + void GenerateThumbnails(string filepath, PropertyType propertyType); } } diff --git a/src/Umbraco.Core/Services/IRedirectUrlService.cs b/src/Umbraco.Core/Services/IRedirectUrlService.cs index 7b21fa9bb7..1249b3b664 100644 --- a/src/Umbraco.Core/Services/IRedirectUrlService.cs +++ b/src/Umbraco.Core/Services/IRedirectUrlService.cs @@ -33,7 +33,7 @@ namespace Umbraco.Core.Services /// Deletes a redirect url. /// /// The redirect url identifier. - void Delete(int id); + void Delete(Guid id); /// /// Deletes all redirect urls. @@ -72,5 +72,15 @@ namespace Umbraco.Core.Services /// The total count of redirect urls. /// The redirect urls. IEnumerable GetAllRedirectUrls(int rootContentId, long pageIndex, int pageSize, out long total); + + /// + /// Searches for all redirect urls that contain a given search term in their URL property. + /// + /// The term to search for. + /// The page index. + /// The page size. + /// The total count of redirect urls. + /// The redirect urls. + IEnumerable SearchRedirectUrls(string searchTerm, long pageIndex, int pageSize, out long total); } } diff --git a/src/Umbraco.Core/Services/MediaService.cs b/src/Umbraco.Core/Services/MediaService.cs index 43aa114e1f..a362b180b1 100644 --- a/src/Umbraco.Core/Services/MediaService.cs +++ b/src/Umbraco.Core/Services/MediaService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Linq; using Umbraco.Core.Events; using Umbraco.Core.IO; @@ -19,15 +20,19 @@ namespace Umbraco.Core.Services /// public class MediaService : RepositoryService, IMediaService, IMediaServiceOperations { + private readonly MediaFileSystem _mediaFileSystem; #region Constructors public MediaService( IDatabaseUnitOfWorkProvider provider, + MediaFileSystem mediaFileSystem, ILogger logger, IEventMessagesFactory eventMessagesFactory) : base(provider, logger, eventMessagesFactory) - { } + { + _mediaFileSystem = mediaFileSystem; + } #endregion @@ -864,7 +869,7 @@ namespace Umbraco.Core.Services var args = new DeleteEventArgs(c, false); // raise event & get flagged files Deleted.RaiseEvent(args, this); - IOHelper.DeleteFiles(args.MediaFilesToDelete, // remove flagged files + _mediaFileSystem.DeleteFiles(args.MediaFilesToDelete, // remove flagged files (file, e) => Logger.Error("An error occurred while deleting file attached to nodes: " + file, e)); } } @@ -1187,6 +1192,43 @@ namespace Umbraco.Core.Services #endregion + #region File Management + + public Stream GetMediaFileContentStream(string filepath) + { + if (_mediaFileSystem.FileExists(filepath) == false) + return null; + + try + { + return _mediaFileSystem.OpenFile(filepath); + } + catch + { + return null; // deal with race conds + } + } + + public void SetMediaFileContent(string filepath, Stream stream) + { + _mediaFileSystem.AddFile(filepath, stream, true); + } + + public void DeleteMediaFile(string filepath) + { + _mediaFileSystem.DeleteFile(filepath, true); + } + + public void GenerateThumbnails(string filepath, PropertyType propertyType) + { + using (var filestream = _mediaFileSystem.OpenFile(filepath)) + { + _mediaFileSystem.GenerateThumbnails(filestream, filepath, propertyType); + } + } + + #endregion + #region Event Handlers /// diff --git a/src/Umbraco.Core/Services/MemberService.cs b/src/Umbraco.Core/Services/MemberService.cs index 1560849107..632dfd3296 100644 --- a/src/Umbraco.Core/Services/MemberService.cs +++ b/src/Umbraco.Core/Services/MemberService.cs @@ -21,6 +21,7 @@ namespace Umbraco.Core.Services public class MemberService : RepositoryService, IMemberService { private readonly IMemberGroupService _memberGroupService; + private readonly MediaFileSystem _mediaFileSystem; #region Constructor @@ -28,11 +29,14 @@ namespace Umbraco.Core.Services IDatabaseUnitOfWorkProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory, - IMemberGroupService memberGroupService) + IMemberGroupService memberGroupService, + MediaFileSystem mediaFileSystem) : base(provider, logger, eventMessagesFactory) { if (memberGroupService == null) throw new ArgumentNullException(nameof(memberGroupService)); + if (mediaFileSystem == null) throw new ArgumentNullException(nameof(mediaFileSystem)); _memberGroupService = memberGroupService; + _mediaFileSystem = mediaFileSystem; } #endregion @@ -949,7 +953,7 @@ namespace Umbraco.Core.Services repository.Delete(member); var args = new DeleteEventArgs(member, false); // raise event & get flagged files Deleted.RaiseEvent(args, this); - IOHelper.DeleteFiles(args.MediaFilesToDelete, // remove flagged files + _mediaFileSystem.DeleteFiles(args.MediaFilesToDelete, // remove flagged files (file, e) => Logger.Error("An error occurred while deleting file attached to nodes: " + file, e)); } diff --git a/src/Umbraco.Core/Services/NotificationService.cs b/src/Umbraco.Core/Services/NotificationService.cs index f0ae977936..9191697329 100644 --- a/src/Umbraco.Core/Services/NotificationService.cs +++ b/src/Umbraco.Core/Services/NotificationService.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -15,8 +16,6 @@ using Umbraco.Core.Models.Membership; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.UnitOfWork; using Umbraco.Core.Strings; -using Umbraco.Core.Persistence; -using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Services { @@ -56,37 +55,72 @@ namespace Umbraco.Core.Services Func createSubject, Func createBody) { - if ((entity is IContent) == false) + if (entity is IContent == false) throw new NotSupportedException(); var content = (IContent) entity; - // lazily get versions - into a list to ensure we can enumerate multiple times - List allVersions = null; + // lazily get previous version + IContentBase prevVersion = null; - long totalUsers; - var allUsers = _userService.GetAll(0, int.MaxValue, out totalUsers); - foreach (var u in allUsers.Where(x => x.IsApproved)) + // do not load *all* users in memory at once + // do not load notifications *per user* (N+1 select) + // cannot load users & notifications in 1 query (combination btw User2AppDto and User2NodeNotifyDto) + // => get batches of users, get all their notifications in 1 query + // re. users: + // users being (dis)approved = not an issue, filtered in memory not in SQL + // users being modified or created = not an issue, ordering by ID, as long as we don't *insert* low IDs + // users being deleted = not an issue for GetNextUsers + var id = 0; + var nodeIds = content.Path.Split(',').Select(int.Parse).ToArray(); + const int pagesz = 400; // load batches of 400 users + do { - var userNotifications = GetUserNotifications(u, content.Path); - var notificationForAction = userNotifications.FirstOrDefault(x => x.Action == action); - if (notificationForAction == null) continue; + // users are returned ordered by id, notifications are returned ordered by user id + var users = ((UserService) _userService).GetNextUsers(id, pagesz).Where(x => x.IsApproved).ToList(); + var notifications = GetUsersNotifications(users.Select(x => x.Id), action, nodeIds, Constants.ObjectTypes.DocumentGuid).ToList(); + if (notifications.Count == 0) break; - if (allVersions == null) // lazy load - allVersions = _contentService.GetVersions(entity.Id).ToList(); - - try + var i = 0; + foreach (var user in users) { - SendNotification(operatingUser, u, content, allVersions, - actionName, http, createSubject, createBody); + // continue if there's no notification for this user + if (notifications[i].UserId != user.Id) continue; // next user - _logger.Debug($"Notification type: {action} sent to {u.Name} ({u.Email})"); + // lazy load prev version + if (prevVersion == null) + { + prevVersion = GetPreviousVersion(entity.Id); + } + + // queue notification + var req = CreateNotificationRequest(operatingUser, user, content, prevVersion, actionName, http, createSubject, createBody); + Enqueue(req); + + // skip other notifications for this user + while (i < notifications.Count && notifications[i++].UserId == user.Id) ; + if (i >= notifications.Count) break; // break if no more notifications } - catch (Exception ex) - { - _logger.Error("An error occurred sending notification", ex); - } - } + + // load more users if any + id = users.Count == pagesz ? users.Last().Id + 1 : -1; + + } while (id > 0); + } + + /// + /// Gets the previous version to the latest version of the content item if there is one + /// + /// + /// + private IContentBase GetPreviousVersion(int contentId) + { + // Regarding this: http://issues.umbraco.org/issue/U4-5180 + // we know they are descending from the service so we know that newest is first + // we are only selecting the top 2 rows since that is all we need + var allVersions = _contentService.GetVersionIds(contentId, 2).ToList(); + var prevVersionIndex = allVersions.Count > 1 ? 1 : 0; + return _contentService.GetByVersion(allVersions[prevVersionIndex]); } /// @@ -106,46 +140,80 @@ namespace Umbraco.Core.Services Func createSubject, Func createBody) { - if ((entities is IEnumerable) == false) + if (entities is IEnumerable == false) throw new NotSupportedException(); - // ensure we can enumerate multiple times var entitiesL = entities as List ?? entities.Cast().ToList(); - // lazily get versions - into lists to ensure we can enumerate multiple times - var allVersionsDictionary = new Dictionary>(); + //exit if there are no entities + if (entitiesL.Count == 0) return; - long totalUsers; - var allUsers = _userService.GetAll(0, int.MaxValue, out totalUsers); - foreach (var u in allUsers.Where(x => x.IsApproved)) + //put all entity's paths into a list with the same indicies + var paths = entitiesL.Select(x => x.Path.Split(',').Select(int.Parse).ToArray()).ToArray(); + + // lazily get versions + var prevVersionDictionary = new Dictionary(); + + // see notes above + var id = 0; + const int pagesz = 400; // load batches of 400 users + do { - var userNotifications = GetUserNotifications(u).ToArray(); + // users are returned ordered by id, notifications are returned ordered by user id + var users = ((UserService)_userService).GetNextUsers(id, pagesz).Where(x => x.IsApproved).ToList(); + var notifications = GetUsersNotifications(users.Select(x => x.Id), action, Enumerable.Empty(), Constants.ObjectTypes.DocumentGuid).ToList(); + if (notifications.Count == 0) break; - foreach (var content in entitiesL) + var i = 0; + foreach (var user in users) { - var userNotificationsByPath = FilterUserNotificationsByPath(userNotifications, content.Path); - var notificationForAction = userNotificationsByPath.FirstOrDefault(x => x.Action == action); - if (notificationForAction == null) continue; + // continue if there's no notification for this user + if (notifications[i].UserId != user.Id) continue; // next user - var allVersions = allVersionsDictionary.ContainsKey(content.Id) // lazy load - ? allVersionsDictionary[content.Id] - : allVersionsDictionary[content.Id] = _contentService.GetVersions(content.Id).ToList(); - - try + for (var j = 0; j < entitiesL.Count; j++) { - SendNotification(operatingUser, u, content, allVersions, - actionName, http, createSubject, createBody); + var content = entitiesL[j]; + var path = paths[j]; + + // test if the notification applies to the path ie to this entity + if (path.Contains(notifications[i].EntityId) == false) continue; // next entity + + if (prevVersionDictionary.ContainsKey(content.Id) == false) + { + prevVersionDictionary[content.Id] = GetPreviousVersion(content.Id); + } + + // queue notification + var req = CreateNotificationRequest(operatingUser, user, content, prevVersionDictionary[content.Id], actionName, http, createSubject, createBody); + Enqueue(req); + } - _logger.Debug($"Notification type: {action} sent to {u.Name} ({u.Email})"); - } - catch (Exception ex) + // skip other notifications for this user, essentially this means moving i to the next index of notifications + // for the next user. + do { - _logger.Error("An error occurred sending notification", ex); - } + i++; + } while (i < notifications.Count && notifications[i].UserId == user.Id); + + if (i >= notifications.Count) break; // break if no more notifications } - } + + // load more users if any + id = users.Count == pagesz ? users.Last().Id + 1 : -1; + + } while (id > 0); } + private IEnumerable GetUsersNotifications(IEnumerable userIds, string action, IEnumerable nodeIds, Guid objectType) + { + using (var uow = _uowProvider.CreateUnitOfWork()) + { + var repository = uow.CreateRepository(); + var notifications = repository.GetUsersNotifications(userIds, action, nodeIds, objectType); + uow.Complete(); + return notifications; + } + } /// /// Gets the notifications for the user /// @@ -186,7 +254,7 @@ namespace Umbraco.Core.Services public IEnumerable FilterUserNotificationsByPath(IEnumerable userNotifications, string path) { var pathParts = path.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries); - return userNotifications.Where(r => pathParts.InvariantContains(r.EntityId.ToString(CultureInfo.InvariantCulture))).ToList(); + return userNotifications.Where(r => pathParts.InvariantContains(r.EntityId.ToString(CultureInfo.InvariantCulture))).ToList(); } /// @@ -294,29 +362,23 @@ namespace Umbraco.Core.Services /// /// /// - /// + /// /// The action readable name - currently an action is just a single letter, this is the name associated with the letter /// /// Callback to create the mail subject /// Callback to create the mail body - private void SendNotification(IUser performingUser, IUser mailingUser, IContent content, IEnumerable allVersions, string actionName, HttpContextBase http, + private NotificationRequest CreateNotificationRequest(IUser performingUser, IUser mailingUser, IContentBase content, IContentBase oldDoc, + string actionName, HttpContextBase http, Func createSubject, Func createBody) { if (performingUser == null) throw new ArgumentNullException("performingUser"); if (mailingUser == null) throw new ArgumentNullException("mailingUser"); if (content == null) throw new ArgumentNullException("content"); - if (allVersions == null) throw new ArgumentNullException("allVersions"); if (http == null) throw new ArgumentNullException("http"); if (createSubject == null) throw new ArgumentNullException("createSubject"); - if (createBody == null) throw new ArgumentNullException("createBody"); - - //Ensure they are sorted: http://issues.umbraco.org/issue/U4-5180 - var allVersionsAsArray = allVersions.OrderBy(x => x.UpdateDate).ToArray(); - - int versionCount = (allVersionsAsArray.Length > 1) ? (allVersionsAsArray.Length - 2) : (allVersionsAsArray.Length - 1); - var oldDoc = _contentService.GetByVersion(allVersionsAsArray[versionCount].Version); - + if (createBody == null) throw new ArgumentNullException("createBody"); + // build summary var summary = new StringBuilder(); var props = content.Properties.ToArray(); @@ -330,16 +392,16 @@ namespace Umbraco.Core.Services { var oldProperty = oldDoc.Properties[p.PropertyType.Alias]; oldText = oldProperty.Value != null ? oldProperty.Value.ToString() : ""; - + // replace html with char equivalent ReplaceHtmlSymbols(ref oldText); ReplaceHtmlSymbols(ref newText); } - + // make sure to only highlight changes done using TinyMCE editor... other changes will be displayed using default summary // TODO: We should probably allow more than just tinymce?? - if ((p.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.TinyMCEAlias) + if ((p.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.TinyMCEAlias) && string.CompareOrdinal(oldText, newText) != 0) { summary.Append(""); @@ -348,26 +410,31 @@ namespace Umbraco.Core.Services " Red for deleted characters Yellow for inserted characters"); summary.Append(""); summary.Append(""); - summary.Append(" New " + - p.PropertyType.Name + ""); - summary.Append("" + - ReplaceLinks(CompareText(oldText, newText, true, false, "", string.Empty), http.Request) + - ""); + summary.Append(" New "); + summary.Append(p.PropertyType.Name); + summary.Append(""); + summary.Append(""); + summary.Append(ReplaceLinks(CompareText(oldText, newText, true, false, "", string.Empty), http.Request)); + summary.Append(""); summary.Append(""); summary.Append(""); - summary.Append(" Old " + - p.PropertyType.Name + ""); - summary.Append("" + - ReplaceLinks(CompareText(newText, oldText, true, false, "", string.Empty), http.Request) + - ""); + summary.Append(" Old "); + summary.Append(p.PropertyType.Name); + summary.Append(""); + summary.Append(""); + summary.Append(ReplaceLinks(CompareText(newText, oldText, true, false, "", string.Empty), http.Request)); + summary.Append(""); summary.Append(""); } else { summary.Append(""); - summary.Append("" + - p.PropertyType.Name + ""); - summary.Append("" + newText + ""); + summary.Append(""); + summary.Append(p.PropertyType.Name); + summary.Append(""); + summary.Append(""); + summary.Append(newText); + summary.Append(""); summary.Append(""); } summary.Append( @@ -378,29 +445,27 @@ namespace Umbraco.Core.Services string[] subjectVars = { - http.Request.ServerVariables["SERVER_NAME"] + ":" + - http.Request.Url.Port + - IOHelper.ResolveUrl(SystemDirectories.Umbraco), + string.Concat(http.Request.ServerVariables["SERVER_NAME"], ":", http.Request.Url.Port, IOHelper.ResolveUrl(SystemDirectories.Umbraco)), actionName, content.Name }; string[] bodyVars = { - mailingUser.Name, - actionName, - content.Name, + mailingUser.Name, + actionName, + content.Name, performingUser.Name, - http.Request.ServerVariables["SERVER_NAME"] + ":" + http.Request.Url.Port + IOHelper.ResolveUrl(SystemDirectories.Umbraco), + string.Concat(http.Request.ServerVariables["SERVER_NAME"], ":", http.Request.Url.Port, IOHelper.ResolveUrl(SystemDirectories.Umbraco)), content.Id.ToString(CultureInfo.InvariantCulture), summary.ToString(), string.Format("{2}://{0}/{1}", - http.Request.ServerVariables["SERVER_NAME"] + ":" + http.Request.Url.Port, + string.Concat(http.Request.ServerVariables["SERVER_NAME"], ":", http.Request.Url.Port), //TODO: RE-enable this so we can have a nice url /*umbraco.library.NiceUrl(documentObject.Id))*/ - content.Id + ".aspx", + string.Concat(content.Id, ".aspx"), protocol) - + }; - // create the mail message + // create the mail message var mail = new MailMessage(UmbracoConfig.For.UmbracoSettings().Content.NotificationEmailAddress, mailingUser.Email); // populate the message @@ -414,10 +479,10 @@ namespace Umbraco.Core.Services { mail.IsBodyHtml = true; mail.Body = - @" + string.Concat(@" -" + createBody(mailingUser, bodyVars); +", createBody(mailingUser, bodyVars)); } // nh, issue 30724. Due to hardcoded http strings in resource files, we need to check for https replacements here @@ -430,32 +495,17 @@ namespace Umbraco.Core.Services string.Format("https://{0}", serverName)); } - - // send it asynchronously, we don't want to got up all of the request time to send emails! - ThreadPool.QueueUserWorkItem(state => - { - try - { - using (mail) - { - using (var sender = new SmtpClient()) - { - sender.Send(mail); - } - } - - } - catch (Exception ex) - { - _logger.Error("An error occurred sending notification", ex); - } - }); + return new NotificationRequest(mail, actionName, mailingUser.Name, mailingUser.Email); } private static string ReplaceLinks(string text, HttpRequestBase request) { - string domain = GlobalSettings.UseSSL ? "https://" : "http://"; - domain += request.ServerVariables["SERVER_NAME"] + ":" + request.Url.Port + "/"; + var sb = new StringBuilder(GlobalSettings.UseSSL ? "https://" : "http://"); + sb.Append(request.ServerVariables["SERVER_NAME"]); + sb.Append(":"); + sb.Append(request.Url.Port); + sb.Append("/"); + var domain = sb.ToString(); text = text.Replace("href=\"/", "href=\"" + domain); text = text.Replace("src=\"/", "src=\"" + domain); return text; @@ -524,7 +574,7 @@ namespace Umbraco.Core.Services pos++; } // while sb.Append(""); - } // if + } // if } // while // write rest of unchanged chars @@ -537,6 +587,93 @@ namespace Umbraco.Core.Services return sb.ToString(); } + // manage notifications + // ideally, would need to use IBackgroundTasks - but they are not part of Core! + + private static readonly object Locker = new object(); + private static readonly BlockingCollection Queue = new BlockingCollection(); + private static volatile bool _running; + + private void Enqueue(NotificationRequest notification) + { + Queue.Add(notification); + if (_running) return; + lock (Locker) + { + if (_running) return; + Process(Queue); + _running = true; + } + } + + private class NotificationRequest + { + public NotificationRequest(MailMessage mail, string action, string userName, string email) + { + Mail = mail; + Action = action; + UserName = userName; + Email = email; + } + + public MailMessage Mail { get; private set; } + + public string Action { get; private set; } + + public string UserName { get; private set; } + + public string Email { get; private set; } + } + + private void Process(BlockingCollection notificationRequests) + { + ThreadPool.QueueUserWorkItem(state => + { + var s = new SmtpClient(); + try + { + _logger.Debug("Begin processing notifications."); + while (true) + { + NotificationRequest request; + while (notificationRequests.TryTake(out request, 8 * 1000)) // stay on for 8s + { + try + { + if (Sendmail != null) Sendmail(s, request.Mail, _logger); else s.Send(request.Mail); + _logger.Debug(string.Format("Notification \"{0}\" sent to {1} ({2})", request.Action, request.UserName, request.Email)); + } + catch (Exception ex) + { + _logger.Error("An error occurred sending notification", ex); + s.Dispose(); + s = new SmtpClient(); + } + finally + { + request.Mail.Dispose(); + } + } + lock (Locker) + { + if (notificationRequests.Count > 0) continue; // last chance + _running = false; // going down + break; + } + } + } + finally + { + s.Dispose(); + } + _logger.Debug("Done processing notifications."); + }); + } + + // for tests + internal static Action Sendmail; + //= (_, msg, logger) => logger.Debug("Email " + msg.To.ToString()); + #endregion } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/RedirectUrlService.cs b/src/Umbraco.Core/Services/RedirectUrlService.cs index 8541fe23a8..7e6de3e356 100644 --- a/src/Umbraco.Core/Services/RedirectUrlService.cs +++ b/src/Umbraco.Core/Services/RedirectUrlService.cs @@ -23,7 +23,7 @@ namespace Umbraco.Core.Services if (redir != null) redir.CreateDateUtc = DateTime.UtcNow; else - redir = new RedirectUrl { Url = url, ContentKey = contentKey }; + redir = new RedirectUrl { Key = Guid.NewGuid(), Url = url, ContentKey = contentKey }; repo.AddOrUpdate(redir); uow.Complete(); } @@ -39,7 +39,7 @@ namespace Umbraco.Core.Services } } - public void Delete(int id) + public void Delete(Guid id) { using (var uow = UowProvider.CreateUnitOfWork()) { @@ -112,5 +112,15 @@ namespace Umbraco.Core.Services return rules; } } + public IEnumerable SearchRedirectUrls(string searchTerm, long pageIndex, int pageSize, out long total) + { + using (var uow = UowProvider.CreateUnitOfWork()) + { + var repo = uow.CreateRepository(); + var rules = repo.SearchUrls(searchTerm, pageIndex, pageSize, out total); + uow.Complete(); + return rules; + } + } } } diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index df56c24997..1fbb343bea 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -526,6 +526,17 @@ namespace Umbraco.Core.Services } } + internal IEnumerable GetNextUsers(int id, int count) + { + using (var uow = UowProvider.CreateUnitOfWork()) + { + var repository = (UserRepository) uow.CreateRepository(); + var users = repository.GetNextUsers(id, count); + uow.Complete(); + return users; + } + } + #endregion #region Implementation of IUserService diff --git a/src/Umbraco.Core/StringExtensions.cs b/src/Umbraco.Core/StringExtensions.cs index 5b7eae36c1..8cb8345046 100644 --- a/src/Umbraco.Core/StringExtensions.cs +++ b/src/Umbraco.Core/StringExtensions.cs @@ -678,31 +678,12 @@ namespace Umbraco.Core return s.LastIndexOf(value, StringComparison.OrdinalIgnoreCase); } - /// - /// Determines if the string is a Guid - /// - /// - /// - /// + [Obsolete("Use Guid.TryParse instead")] + [EditorBrowsable(EditorBrowsableState.Never)] public static bool IsGuid(this string str, bool withHyphens) { - var isGuid = false; - - if (!String.IsNullOrEmpty(str)) - { - Regex guidRegEx; - if (withHyphens) - { - guidRegEx = new Regex(@"^(\{{0,1}([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}\}{0,1})$"); - } - else - { - guidRegEx = new Regex(@"^(\{{0,1}([0-9a-fA-F]){8}([0-9a-fA-F]){4}([0-9a-fA-F]){4}([0-9a-fA-F]){4}([0-9a-fA-F]){12}\}{0,1})$"); - } - isGuid = guidRegEx.IsMatch(str); - } - - return isGuid; + Guid g; + return Guid.TryParse(str, out g); } /// @@ -724,7 +705,7 @@ namespace Umbraco.Core /// public static object ParseInto(this string val, Type type) { - if (!String.IsNullOrEmpty(val)) + if (string.IsNullOrEmpty(val) == false) { TypeConverter tc = TypeDescriptor.GetConverter(type); return tc.ConvertFrom(val); @@ -762,6 +743,36 @@ namespace Umbraco.Core return stringBuilder.ToString(); } + /// + /// Converts the string to SHA1 + /// + /// referrs to itself + /// the md5 hashed string + public static string ToSHA1(this string stringToConvert) + { + //create an instance of the SHA1CryptoServiceProvider + var md5Provider = new SHA1CryptoServiceProvider(); + + //convert our string into byte array + var byteArray = Encoding.UTF8.GetBytes(stringToConvert); + + //get the hashed values created by our SHA1CryptoServiceProvider + var hashedByteArray = md5Provider.ComputeHash(byteArray); + + //create a StringBuilder object + var stringBuilder = new StringBuilder(); + + //loop to each each byte + foreach (var b in hashedByteArray) + { + //append it to our StringBuilder + stringBuilder.Append(b.ToString("x2").ToLower()); + } + + //return the hashed value + return stringBuilder.ToString(); + } + /// /// Decodes a string that was encoded with UrlTokenEncode diff --git a/src/Umbraco.Core/Strings/IUrlSegmentProvider.cs b/src/Umbraco.Core/Strings/IUrlSegmentProvider.cs index dc3f4243e7..c3ef596ad0 100644 --- a/src/Umbraco.Core/Strings/IUrlSegmentProvider.cs +++ b/src/Umbraco.Core/Strings/IUrlSegmentProvider.cs @@ -26,5 +26,9 @@ namespace Umbraco.Core.Strings /// per content, in 1-to-1 multilingual configurations. Then there would be one /// url per culture. string GetUrlSegment(IContentBase content, CultureInfo culture); + + //TODO: For the 301 tracking, we need to add another extended interface to this so that + // the RedirectTrackingEventHandler can ask the IUrlSegmentProvider if the URL is changing. + // Currently the way it works is very hacky, see notes in: RedirectTrackingEventHandler.ContentService_Publishing } } diff --git a/src/Umbraco.Core/Sync/ApplicationUrlHelper.cs b/src/Umbraco.Core/Sync/ApplicationUrlHelper.cs index c9d9b60ff9..1ec9898618 100644 --- a/src/Umbraco.Core/Sync/ApplicationUrlHelper.cs +++ b/src/Umbraco.Core/Sync/ApplicationUrlHelper.cs @@ -113,7 +113,8 @@ namespace Umbraco.Core.Sync ? ":" + request.ServerVariables["SERVER_PORT"] : ""; - var ssl = GlobalSettings.UseSSL ? "s" : ""; // force, whatever the first request + var useSsl = GlobalSettings.UseSSL || port == "443"; + var ssl = useSsl ? "s" : ""; // force, whatever the first request var url = "http" + ssl + "://" + request.ServerVariables["SERVER_NAME"] + port + IOHelper.ResolveUrl(SystemDirectories.Umbraco); return url.TrimEnd('/'); diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index e98b778b3f..3a0f8ad9c8 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -41,6 +41,7 @@ + @@ -69,6 +70,7 @@ + @@ -280,9 +282,13 @@ Files.resx + + + + @@ -301,6 +307,9 @@ + + + @@ -315,6 +324,7 @@ + @@ -331,6 +341,7 @@ + @@ -368,9 +379,12 @@ + + + @@ -382,9 +396,6 @@ - - - @@ -449,12 +460,14 @@ + + @@ -477,7 +490,6 @@ - @@ -488,21 +500,23 @@ + + + + - - @@ -558,7 +572,6 @@ - @@ -1046,7 +1059,6 @@ - @@ -1304,7 +1316,7 @@ - + diff --git a/src/Umbraco.Core/Xml/XmlHelper.cs b/src/Umbraco.Core/Xml/XmlHelper.cs index 17fe6fa526..3fa5c0ebc8 100644 --- a/src/Umbraco.Core/Xml/XmlHelper.cs +++ b/src/Umbraco.Core/Xml/XmlHelper.cs @@ -34,7 +34,7 @@ namespace Umbraco.Core.Xml return; } if (n.Attributes[name] == null) - { + { var a = xml.CreateAttribute(name); a.Value = value; n.Attributes.Append(a); @@ -118,7 +118,7 @@ namespace Umbraco.Core.Xml //var name = nav.LocalName; // must not match an excluded tag //if (UmbracoConfig.For.UmbracoSettings().Scripting.NotDynamicXmlDocumentElements.All(x => x.Element.InvariantEquals(name) == false)) return true; - + return true; } @@ -157,14 +157,14 @@ namespace Umbraco.Core.Xml // used apart from for tests so don't think this matters. In any case, we no longer check for this! //var name = elt.Name.LocalName; // must not match an excluded tag - //if (UmbracoConfig.For.UmbracoSettings().Scripting.NotDynamicXmlDocumentElements.All(x => x.Element.InvariantEquals(name) == false)) + //if (UmbracoConfig.For.UmbracoSettings().Scripting.NotDynamicXmlDocumentElements.All(x => x.Element.InvariantEquals(name) == false)) // return true; //elt = null; //return false; return true; } - + /// /// Sorts the children of a parentNode. /// @@ -172,7 +172,7 @@ namespace Umbraco.Core.Xml /// An XPath expression to select children of to sort. /// A function returning the value to order the nodes by. internal static void SortNodes( - XmlNode parentNode, + XmlNode parentNode, string childNodesXPath, Func orderBy) { @@ -236,7 +236,7 @@ namespace Umbraco.Core.Xml /// The child node to sort. /// A function returning the value to order the nodes by. /// A value indicating whether sorting was needed. - /// Assuming all nodes but are sorted, this will move the node to + /// Assuming all nodes but are sorted, this will move the node to /// the right position without moving all the nodes (as SortNodes would do) - should improve perfs. internal static bool SortNode( XmlNode parentNode, @@ -424,6 +424,25 @@ namespace Umbraco.Core.Xml return AddTextNode(xd, name, value); } + /// + /// Sets or creates an Xml node from its inner Xml. + /// + /// The xmldocument. + /// The node to set or create the child text node on + /// The node name. + /// The node inner Xml. + /// a XmlNode + public static XmlNode SetInnerXmlNode(XmlDocument xd, XmlNode parent, string name, string value) + { + if (xd == null) throw new ArgumentNullException(nameof(xd)); + if (parent == null) throw new ArgumentNullException(nameof(parent)); + if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(name)); + + var child = parent.SelectSingleNode(name) ?? xd.CreateNode(XmlNodeType.Element, name, ""); + child.InnerXml = value; + return child; + } + /// /// Creates a cdata XmlNode with the specified name and value /// @@ -431,7 +450,7 @@ namespace Umbraco.Core.Xml /// The node name. /// The node value. /// A XmlNode - public static XmlNode AddCDataNode(XmlDocument xd, string name, string value) + public static XmlNode AddCDataNode(XmlDocument xd, string name, string value) { if (xd == null) throw new ArgumentNullException("xd"); if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullOrEmptyException(nameof(name)); diff --git a/src/Umbraco.Tests/CallContextTests.cs b/src/Umbraco.Tests/CallContextTests.cs new file mode 100644 index 0000000000..0c2c6dd203 --- /dev/null +++ b/src/Umbraco.Tests/CallContextTests.cs @@ -0,0 +1,84 @@ +using System.Runtime.Remoting.Messaging; +using NUnit.Framework; +using Umbraco.Core; + +namespace Umbraco.Tests +{ + [TestFixture] + public class CallContextTests + { + private static bool _first; + + static CallContextTests() + { + SafeCallContext.Register(() => + { + CallContext.FreeNamedDataSlot("test1"); + CallContext.FreeNamedDataSlot("test2"); + return null; + }, o => {}); + } + + [TestFixtureSetUp] + public void SetUpFixture() + { + _first = true; + } + + // logical call context leaks between tests + // is is required to clear it before tests begin + // (don't trust other tests properly tearing down) + + [SetUp] + public void Setup() + { + SafeCallContext.Clear(); + } + + //[TearDown] + //public void TearDown() + //{ + // SafeCallContext.Clear(); + //} + + [Test] + public void Test1() + { + CallContext.LogicalSetData("test1", "test1"); + Assert.IsNull(CallContext.LogicalGetData("test2")); + + CallContext.LogicalSetData("test3b", "test3b"); + + if (_first) + { + _first = false; + } + else + { + Assert.IsNotNull(CallContext.LogicalGetData("test3a")); // leak! + } + } + + [Test] + public void Test2() + { + CallContext.LogicalSetData("test2", "test2"); + Assert.IsNull(CallContext.LogicalGetData("test1")); + } + + [Test] + public void Test3() + { + CallContext.LogicalSetData("test3a", "test3a"); + + if (_first) + { + _first = false; + } + else + { + Assert.IsNotNull(CallContext.LogicalGetData("test3b")); // leak! + } + } + } +} diff --git a/src/Umbraco.Tests/Configurations/UmbracoSettings/WebRoutingElementDefaultTests.cs b/src/Umbraco.Tests/Configurations/UmbracoSettings/WebRoutingElementDefaultTests.cs index 47b5e15b69..1e568c608e 100644 --- a/src/Umbraco.Tests/Configurations/UmbracoSettings/WebRoutingElementDefaultTests.cs +++ b/src/Umbraco.Tests/Configurations/UmbracoSettings/WebRoutingElementDefaultTests.cs @@ -28,5 +28,11 @@ namespace Umbraco.Tests.Configurations.UmbracoSettings { Assert.IsTrue(SettingsSection.WebRouting.DisableFindContentByIdPath == false); } + + [Test] + public void DisableRedirectUrlTracking() + { + Assert.IsTrue(SettingsSection.WebRouting.DisableRedirectUrlTracking == false); + } } } \ No newline at end of file diff --git a/src/Umbraco.Tests/FrontEnd/UmbracoHelperTests.cs b/src/Umbraco.Tests/FrontEnd/UmbracoHelperTests.cs index 035a76d052..672b9022e5 100644 --- a/src/Umbraco.Tests/FrontEnd/UmbracoHelperTests.cs +++ b/src/Umbraco.Tests/FrontEnd/UmbracoHelperTests.cs @@ -1,9 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Collections.Generic; using NUnit.Framework; +using Umbraco.Core; using Umbraco.Web; namespace Umbraco.Tests.FrontEnd @@ -48,5 +45,38 @@ namespace Umbraco.Tests.FrontEnd Assert.AreEqual("Hello world, this is some text with…", result); } + [Test] + public void Create_Encrypted_RouteString_From_Anonymous_Object() + { + var additionalRouteValues = new + { + key1 = "value1", + key2 = "value2", + Key3 = "Value3", + keY4 = "valuE4" + }; + var encryptedRouteString = UmbracoHelper.CreateEncryptedRouteString("FormController", "FormAction", "", additionalRouteValues); + var result = encryptedRouteString.DecryptWithMachineKey(); + var expectedResult = "c=FormController&a=FormAction&ar=&key1=value1&key2=value2&Key3=Value3&keY4=valuE4"; + + Assert.AreEqual(expectedResult, result); + } + + [Test] + public void Create_Encrypted_RouteString_From_Dictionary() + { + var additionalRouteValues = new Dictionary() + { + {"key1", "value1"}, + {"key2", "value2"}, + {"Key3", "Value3"}, + {"keY4", "valuE4"} + }; + var encryptedRouteString = UmbracoHelper.CreateEncryptedRouteString("FormController", "FormAction", "", additionalRouteValues); + var result = encryptedRouteString.DecryptWithMachineKey(); + var expectedResult = "c=FormController&a=FormAction&ar=&key1=value1&key2=value2&Key3=Value3&keY4=valuE4"; + + Assert.AreEqual(expectedResult, result); + } } } diff --git a/src/Umbraco.Tests/IO/FileSystemProviderManagerTests.cs b/src/Umbraco.Tests/IO/FileSystemProviderManagerTests.cs index 3350b8b65e..e4fc8dd890 100644 --- a/src/Umbraco.Tests/IO/FileSystemProviderManagerTests.cs +++ b/src/Umbraco.Tests/IO/FileSystemProviderManagerTests.cs @@ -1,11 +1,9 @@ using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; +using Moq; using NUnit.Framework; using Umbraco.Core; using Umbraco.Core.IO; +using Umbraco.Core.Logging; using Umbraco.Tests.TestHelpers; namespace Umbraco.Tests.IO @@ -24,23 +22,26 @@ namespace Umbraco.Tests.IO [Test] public void Can_Get_Base_File_System() { - var fs = FileSystemProviderManager.Current.GetUnderlyingFileSystemProvider(Constants.IO.MediaFileSystemProvider); + var fileSystems = new FileSystems(Mock.Of()); + var fileSystem = fileSystems.GetUnderlyingFileSystemProvider(Constants.IO.MediaFileSystemProvider); - Assert.NotNull(fs); + Assert.NotNull(fileSystem); } [Test] public void Can_Get_Typed_File_System() { - var fs = FileSystemProviderManager.Current.GetFileSystemProvider(); + var fileSystems = new FileSystems(Mock.Of()); + var fileSystem = fileSystems.GetFileSystem(); - Assert.NotNull(fs); + Assert.NotNull(fileSystem); } [Test] public void Exception_Thrown_On_Invalid_Typed_File_System() { - Assert.Throws(() => FileSystemProviderManager.Current.GetFileSystemProvider()); + var fileSystems = new FileSystems(Mock.Of()); + Assert.Throws(() => fileSystems.GetFileSystem()); } @@ -54,6 +55,5 @@ namespace Umbraco.Tests.IO { } } - } } diff --git a/src/Umbraco.Tests/IO/ShadowFileSystemTests.cs b/src/Umbraco.Tests/IO/ShadowFileSystemTests.cs new file mode 100644 index 0000000000..bbac9cc6fc --- /dev/null +++ b/src/Umbraco.Tests/IO/ShadowFileSystemTests.cs @@ -0,0 +1,592 @@ +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using Moq; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; + +namespace Umbraco.Tests.IO +{ + [TestFixture] + public class ShadowFileSystemTests + { + [SetUp] + public void SetUp() + { + SafeCallContext.Clear(); + ClearFiles(); + } + + [TearDown] + public void TearDown() + { + SafeCallContext.Clear(); + ClearFiles(); + } + + private static void ClearFiles() + { + var path = IOHelper.MapPath("FileSysTests"); + if (Directory.Exists(path)) + { + foreach (var file in Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories)) + File.Delete(file); + Directory.Delete(path, true); + } + path = IOHelper.MapPath("App_Data"); + if (Directory.Exists(path)) + { + foreach (var file in Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories)) + File.Delete(file); + Directory.Delete(path, true); + } + } + + private static string NormPath(string path) + { + return path.ToLowerInvariant().Replace("\\", "/"); + } + + [Test] + public void ShadowDeleteDirectory() + { + var path = IOHelper.MapPath("FileSysTests"); + Directory.CreateDirectory(path); + Directory.CreateDirectory(path + "/ShadowTests"); + Directory.CreateDirectory(path + "/ShadowSystem"); + + var fs = new PhysicalFileSystem(path + "/ShadowTests/", "ignore"); + var sfs = new PhysicalFileSystem(path + "/ShadowSystem/", "ignore"); + var ss = new ShadowFileSystem(fs, sfs); + + Directory.CreateDirectory(path + "/ShadowTests/d1"); + Directory.CreateDirectory(path + "/ShadowTests/d2"); + + var files = fs.GetFiles(""); + Assert.AreEqual(0, files.Count()); + + var dirs = fs.GetDirectories(""); + Assert.AreEqual(2, dirs.Count()); + Assert.IsTrue(dirs.Contains("d1")); + Assert.IsTrue(dirs.Contains("d2")); + + ss.DeleteDirectory("d1"); + + Assert.IsTrue(Directory.Exists(path + "/ShadowTests/d1")); + Assert.IsTrue(fs.DirectoryExists("d1")); + Assert.IsFalse(ss.DirectoryExists("d1")); + + dirs = ss.GetDirectories(""); + Assert.AreEqual(1, dirs.Count()); + Assert.IsTrue(dirs.Contains("d2")); + } + + [Test] + public void ShadowDeleteDirectoryInDir() + { + var path = IOHelper.MapPath("FileSysTests"); + Directory.CreateDirectory(path); + Directory.CreateDirectory(path + "/ShadowTests"); + Directory.CreateDirectory(path + "/ShadowSystem"); + + var fs = new PhysicalFileSystem(path + "/ShadowTests/", "ignore"); + var sfs = new PhysicalFileSystem(path + "/ShadowSystem/", "ignore"); + var ss = new ShadowFileSystem(fs, sfs); + + Directory.CreateDirectory(path + "/ShadowTests/sub"); + Directory.CreateDirectory(path + "/ShadowTests/sub/d1"); + Directory.CreateDirectory(path + "/ShadowTests/sub/d2"); + + var files = fs.GetFiles(""); + Assert.AreEqual(0, files.Count()); + + var dirs = ss.GetDirectories(""); + Assert.AreEqual(1, dirs.Count()); + Assert.IsTrue(dirs.Contains("sub")); + + dirs = fs.GetDirectories("sub"); + Assert.AreEqual(2, dirs.Count()); + Assert.IsTrue(dirs.Contains("sub/d1")); + Assert.IsTrue(dirs.Contains("sub/d2")); + + dirs = ss.GetDirectories("sub"); + Assert.AreEqual(2, dirs.Count()); + Assert.IsTrue(dirs.Contains("sub/d1")); + Assert.IsTrue(dirs.Contains("sub/d2")); + + ss.DeleteDirectory("sub/d1"); + + Assert.IsTrue(Directory.Exists(path + "/ShadowTests/sub/d1")); + Assert.IsTrue(fs.DirectoryExists("sub/d1")); + Assert.IsFalse(ss.DirectoryExists("sub/d1")); + + dirs = fs.GetDirectories("sub"); + Assert.AreEqual(2, dirs.Count()); + Assert.IsTrue(dirs.Contains("sub/d1")); + Assert.IsTrue(dirs.Contains("sub/d2")); + + dirs = ss.GetDirectories("sub"); + Assert.AreEqual(1, dirs.Count()); + Assert.IsTrue(dirs.Contains("sub/d2")); + } + + [Test] + public void ShadowDeleteFile() + { + var path = IOHelper.MapPath("FileSysTests"); + Directory.CreateDirectory(path); + Directory.CreateDirectory(path + "/ShadowTests"); + Directory.CreateDirectory(path + "/ShadowSystem"); + + var fs = new PhysicalFileSystem(path + "/ShadowTests/", "ignore"); + var sfs = new PhysicalFileSystem(path + "/ShadowSystem/", "ignore"); + var ss = new ShadowFileSystem(fs, sfs); + + File.WriteAllText(path + "/ShadowTests/f1.txt", "foo"); + File.WriteAllText(path + "/ShadowTests/f2.txt", "foo"); + + var files = fs.GetFiles(""); + Assert.AreEqual(2, files.Count()); + Assert.IsTrue(files.Contains("f1.txt")); + Assert.IsTrue(files.Contains("f2.txt")); + + files = ss.GetFiles(""); + Assert.AreEqual(2, files.Count()); + Assert.IsTrue(files.Contains("f1.txt")); + Assert.IsTrue(files.Contains("f2.txt")); + + var dirs = ss.GetDirectories(""); + Assert.AreEqual(0, dirs.Count()); + + ss.DeleteFile("f1.txt"); + + Assert.IsTrue(File.Exists(path + "/ShadowTests/f1.txt")); + Assert.IsTrue(fs.FileExists("f1.txt")); + Assert.IsFalse(ss.FileExists("f1.txt")); + + files = ss.GetFiles(""); + Assert.AreEqual(1, files.Count()); + Assert.IsTrue(files.Contains("f2.txt")); + } + + [Test] + public void ShadowDeleteFileInDir() + { + var path = IOHelper.MapPath("FileSysTests"); + Directory.CreateDirectory(path); + Directory.CreateDirectory(path + "/ShadowTests"); + Directory.CreateDirectory(path + "/ShadowSystem"); + + var fs = new PhysicalFileSystem(path + "/ShadowTests/", "ignore"); + var sfs = new PhysicalFileSystem(path + "/ShadowSystem/", "ignore"); + var ss = new ShadowFileSystem(fs, sfs); + + Directory.CreateDirectory(path + "/ShadowTests/sub"); + File.WriteAllText(path + "/ShadowTests/sub/f1.txt", "foo"); + File.WriteAllText(path + "/ShadowTests/sub/f2.txt", "foo"); + + var files = fs.GetFiles(""); + Assert.AreEqual(0, files.Count()); + + files = fs.GetFiles("sub"); + Assert.AreEqual(2, files.Count()); + Assert.IsTrue(files.Contains("sub/f1.txt")); + Assert.IsTrue(files.Contains("sub/f2.txt")); + + files = ss.GetFiles(""); + Assert.AreEqual(0, files.Count()); + + var dirs = ss.GetDirectories(""); + Assert.AreEqual(1, dirs.Count()); + Assert.IsTrue(dirs.Contains("sub")); + + files = ss.GetFiles("sub"); + Assert.AreEqual(2, files.Count()); + Assert.IsTrue(files.Contains("sub/f1.txt")); + Assert.IsTrue(files.Contains("sub/f2.txt")); + + dirs = ss.GetDirectories("sub"); + Assert.AreEqual(0, dirs.Count()); + + ss.DeleteFile("sub/f1.txt"); + + Assert.IsTrue(File.Exists(path + "/ShadowTests/sub/f1.txt")); + Assert.IsTrue(fs.FileExists("sub/f1.txt")); + Assert.IsFalse(ss.FileExists("sub/f1.txt")); + + files = fs.GetFiles("sub"); + Assert.AreEqual(2, files.Count()); + Assert.IsTrue(files.Contains("sub/f1.txt")); + Assert.IsTrue(files.Contains("sub/f2.txt")); + + files = ss.GetFiles("sub"); + Assert.AreEqual(1, files.Count()); + Assert.IsTrue(files.Contains("sub/f2.txt")); + } + + [Test] + public void ShadowCantCreateFile() + { + var path = IOHelper.MapPath("FileSysTests"); + Directory.CreateDirectory(path); + Directory.CreateDirectory(path + "/ShadowTests"); + Directory.CreateDirectory(path + "/ShadowSystem"); + + var fs = new PhysicalFileSystem(path + "/ShadowTests/", "ignore"); + var sfs = new PhysicalFileSystem(path + "/ShadowSystem/", "ignore"); + var ss = new ShadowFileSystem(fs, sfs); + + Assert.Throws(() => + { + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) + ss.AddFile("../../f1.txt", ms); + }); + } + + [Test] + public void ShadowCreateFile() + { + var path = IOHelper.MapPath("FileSysTests"); + Directory.CreateDirectory(path); + Directory.CreateDirectory(path + "/ShadowTests"); + Directory.CreateDirectory(path + "/ShadowSystem"); + + var fs = new PhysicalFileSystem(path + "/ShadowTests/", "ignore"); + var sfs = new PhysicalFileSystem(path + "/ShadowSystem/", "ignore"); + var ss = new ShadowFileSystem(fs, sfs); + + File.WriteAllText(path + "/ShadowTests/f2.txt", "foo"); + + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) + ss.AddFile("f1.txt", ms); + + Assert.IsTrue(File.Exists(path + "/ShadowTests/f2.txt")); + Assert.IsFalse(File.Exists(path + "/ShadowSystem/f2.txt")); + Assert.IsTrue(fs.FileExists("f2.txt")); + Assert.IsTrue(ss.FileExists("f2.txt")); + + Assert.IsFalse(File.Exists(path + "/ShadowTests/f1.txt")); + Assert.IsTrue(File.Exists(path + "/ShadowSystem/f1.txt")); + Assert.IsFalse(fs.FileExists("f1.txt")); + Assert.IsTrue(ss.FileExists("f1.txt")); + + var files = ss.GetFiles(""); + Assert.AreEqual(2, files.Count()); + Assert.IsTrue(files.Contains("f1.txt")); + Assert.IsTrue(files.Contains("f2.txt")); + + string content; + using (var stream = ss.OpenFile("f1.txt")) + content = new StreamReader(stream).ReadToEnd(); + + Assert.AreEqual("foo", content); + } + + [Test] + public void ShadowCreateFileInDir() + { + var path = IOHelper.MapPath("FileSysTests"); + Directory.CreateDirectory(path); + Directory.CreateDirectory(path + "/ShadowTests"); + Directory.CreateDirectory(path + "/ShadowSystem"); + + var fs = new PhysicalFileSystem(path + "/ShadowTests/", "ignore"); + var sfs = new PhysicalFileSystem(path + "/ShadowSystem/", "ignore"); + var ss = new ShadowFileSystem(fs, sfs); + + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) + ss.AddFile("sub/f1.txt", ms); + + Assert.IsFalse(File.Exists(path + "/ShadowTests/sub/f1.txt")); + Assert.IsTrue(File.Exists(path + "/ShadowSystem/sub/f1.txt")); + Assert.IsFalse(fs.FileExists("sub/f1.txt")); + Assert.IsTrue(ss.FileExists("sub/f1.txt")); + + Assert.IsFalse(fs.DirectoryExists("sub")); + Assert.IsTrue(ss.DirectoryExists("sub")); + + var dirs = fs.GetDirectories(""); + Assert.AreEqual(0, dirs.Count()); + + dirs = ss.GetDirectories(""); + Assert.AreEqual(1, dirs.Count()); + Assert.IsTrue(dirs.Contains("sub")); + + var files = ss.GetFiles("sub"); + Assert.AreEqual(1, files.Count()); + + string content; + using (var stream = ss.OpenFile("sub/f1.txt")) + content = new StreamReader(stream).ReadToEnd(); + + Assert.AreEqual("foo", content); + } + + [Test] + public void ShadowAbort() + { + var path = IOHelper.MapPath("FileSysTests"); + Directory.CreateDirectory(path); + Directory.CreateDirectory(path + "/ShadowTests"); + Directory.CreateDirectory(path + "/ShadowSystem"); + + var fs = new PhysicalFileSystem(path + "/ShadowTests/", "ignore"); + var sfs = new PhysicalFileSystem(path + "/ShadowSystem/", "ignore"); + var ss = new ShadowFileSystem(fs, sfs); + + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) + ss.AddFile("path/to/some/dir/f1.txt", ms); + + Assert.IsTrue(File.Exists(path + "/ShadowSystem/path/to/some/dir/f1.txt")); + + // kill everything and let the shadow fs die + Directory.Delete(path + "/ShadowSystem", true); + } + + [Test] + public void ShadowComplete() + { + var path = IOHelper.MapPath("FileSysTests"); + Directory.CreateDirectory(path); + Directory.CreateDirectory(path + "/ShadowTests"); + Directory.CreateDirectory(path + "/ShadowSystem"); + + var fs = new PhysicalFileSystem(path + "/ShadowTests/", "ignore"); + var sfs = new PhysicalFileSystem(path + "/ShadowSystem/", "ignore"); + var ss = new ShadowFileSystem(fs, sfs); + + Directory.CreateDirectory(path + "/ShadowTests/sub/sub"); + File.WriteAllText(path + "/ShadowTests/sub/sub/f2.txt", "foo"); + + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) + ss.AddFile("path/to/some/dir/f1.txt", ms); + ss.DeleteFile("sub/sub/f2.txt"); + + Assert.IsTrue(File.Exists(path + "/ShadowSystem/path/to/some/dir/f1.txt")); + + ss.Complete(); + + Assert.IsTrue(File.Exists(path + "/ShadowSystem/path/to/some/dir/f1.txt")); // *not* cleaning + Assert.IsTrue(File.Exists(path + "/ShadowTests/path/to/some/dir/f1.txt")); + Assert.IsFalse(File.Exists(path + "/ShadowTests/sub/sub/f2.txt")); + } + + [Test] + public void ShadowScopeComplete() + { + var logger = Mock.Of(); + + var path = IOHelper.MapPath("FileSysTests"); + var appdata = IOHelper.MapPath("App_Data"); + Directory.CreateDirectory(path); + + var fs = new PhysicalFileSystem(path, "ignore"); + var sw = new ShadowWrapper(fs, "shadow"); + var swa = new[] { sw }; + + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) + sw.AddFile("sub/f1.txt", ms); + Assert.IsTrue(fs.FileExists("sub/f1.txt")); + + Guid id; + + // explicit shadow without scope does not work + sw.Shadow(id = Guid.NewGuid()); + Assert.IsTrue(Directory.Exists(appdata + "/Shadow/" + id)); + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) + sw.AddFile("sub/f2.txt", ms); + Assert.IsTrue(fs.FileExists("sub/f2.txt")); + sw.UnShadow(true); + Assert.IsTrue(fs.FileExists("sub/f2.txt")); + Assert.IsFalse(Directory.Exists(appdata + "/Shadow/" + id)); + + // shadow with scope but no complete does not complete + var scope = ShadowFileSystemsScope.CreateScope(id = Guid.NewGuid(), swa, logger); + Assert.IsTrue(Directory.Exists(appdata + "/Shadow/" + id)); + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) + sw.AddFile("sub/f3.txt", ms); + Assert.IsFalse(fs.FileExists("sub/f3.txt")); + Assert.AreEqual(1, Directory.GetDirectories(appdata + "/Shadow").Length); + scope.Dispose(); + Assert.IsFalse(fs.FileExists("sub/f3.txt")); + Assert.IsFalse(Directory.Exists(appdata + "/Shadow/" + id)); + + // shadow with scope and complete does complete + scope = ShadowFileSystemsScope.CreateScope(id = Guid.NewGuid(), swa, logger); + Assert.IsTrue(Directory.Exists(appdata + "/Shadow/" + id)); + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) + sw.AddFile("sub/f4.txt", ms); + Assert.IsFalse(fs.FileExists("sub/f4.txt")); + Assert.AreEqual(1, Directory.GetDirectories(appdata + "/Shadow").Length); + scope.Complete(); + Assert.IsTrue(fs.FileExists("sub/f4.txt")); + Assert.AreEqual(0, Directory.GetDirectories(appdata + "/Shadow").Length); + scope.Dispose(); + Assert.IsTrue(fs.FileExists("sub/f4.txt")); + Assert.IsFalse(Directory.Exists(appdata + "/Shadow/" + id)); + + // test scope for "another thread" + + scope = ShadowFileSystemsScope.CreateScope(id = Guid.NewGuid(), swa, logger); + Assert.IsTrue(Directory.Exists(appdata + "/Shadow/" + id)); + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) + sw.AddFile("sub/f5.txt", ms); + Assert.IsFalse(fs.FileExists("sub/f5.txt")); + using (new SafeCallContext()) // pretend we're another thread w/out scope + { + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) + sw.AddFile("sub/f6.txt", ms); + } + Assert.IsTrue(fs.FileExists("sub/f6.txt")); // other thread has written out to fs + scope.Complete(); + Assert.IsTrue(fs.FileExists("sub/f5.txt")); + scope.Dispose(); + Assert.IsTrue(fs.FileExists("sub/f5.txt")); + Assert.IsFalse(Directory.Exists(appdata + "/Shadow/" + id)); + } + + [Test] + public void ShadowScopeCompleteWithFileConflict() + { + var path = IOHelper.MapPath("FileSysTests"); + var appdata = IOHelper.MapPath("App_Data"); + Directory.CreateDirectory(path); + + var fs = new PhysicalFileSystem(path, "ignore"); + var sw = new ShadowWrapper(fs, "shadow"); + var swa = new[] { sw }; + + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) + sw.AddFile("sub/f1.txt", ms); + Assert.IsTrue(fs.FileExists("sub/f1.txt")); + + Guid id; + + var scope = ShadowFileSystemsScope.CreateScope(id = Guid.NewGuid(), swa, Mock.Of()); + Assert.IsTrue(Directory.Exists(appdata + "/Shadow/" + id)); + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) + sw.AddFile("sub/f2.txt", ms); + Assert.IsFalse(fs.FileExists("sub/f2.txt")); + using (new SafeCallContext()) // pretend we're another thread w/out scope + { + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("bar"))) + sw.AddFile("sub/f2.txt", ms); + } + Assert.IsTrue(fs.FileExists("sub/f2.txt")); // other thread has written out to fs + scope.Complete(); + Assert.IsTrue(fs.FileExists("sub/f2.txt")); + scope.Dispose(); + Assert.IsTrue(fs.FileExists("sub/f2.txt")); + Assert.IsFalse(Directory.Exists(appdata + "/Shadow/" + id)); + + string text; + using (var s = fs.OpenFile("sub/f2.txt")) + using (var r = new StreamReader(s)) + text = r.ReadToEnd(); + + // the shadow filesystem will happily overwrite anything it can + Assert.AreEqual("foo", text); + } + + [Test] + public void ShadowScopeCompleteWithDirectoryConflict() + { + var path = IOHelper.MapPath("FileSysTests"); + var appdata = IOHelper.MapPath("App_Data"); + Directory.CreateDirectory(path); + + var fs = new PhysicalFileSystem(path, "ignore"); + var sw = new ShadowWrapper(fs, "shadow"); + var swa = new[] { sw }; + + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) + sw.AddFile("sub/f1.txt", ms); + Assert.IsTrue(fs.FileExists("sub/f1.txt")); + + Guid id; + + var scope = ShadowFileSystemsScope.CreateScope(id = Guid.NewGuid(), swa, Mock.Of()); + Assert.IsTrue(Directory.Exists(appdata + "/Shadow/" + id)); + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) + sw.AddFile("sub/f2.txt", ms); + Assert.IsFalse(fs.FileExists("sub/f2.txt")); + using (new SafeCallContext()) // pretend we're another thread w/out scope + { + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("bar"))) + sw.AddFile("sub/f2.txt/f2.txt", ms); + } + Assert.IsTrue(fs.FileExists("sub/f2.txt/f2.txt")); // other thread has written out to fs + + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) + sw.AddFile("sub/f3.txt", ms); + Assert.IsFalse(fs.FileExists("sub/f3.txt")); + + try + { + // no way this can work since we're trying to write a file + // but there's now a directory with the same name on the real fs + scope.Complete(); + Assert.Fail("Expected AggregateException."); + } + catch (AggregateException ae) + { + Assert.AreEqual(1, ae.InnerExceptions.Count); + var e = ae.InnerExceptions[0]; + Assert.IsNotNull(e.InnerException); + Assert.IsInstanceOf(e); + ae = (AggregateException) e; + + Assert.AreEqual(1, ae.InnerExceptions.Count); + e = ae.InnerExceptions[0]; + Assert.IsNotNull(e.InnerException); + Assert.IsInstanceOf(e.InnerException); + } + + // still, the rest of the changes has been applied ok + Assert.IsTrue(fs.FileExists("sub/f3.txt")); + } + + [Test] + public void GetFilesReturnsChildrenOnly() + { + var path = IOHelper.MapPath("FileSysTests"); + Directory.CreateDirectory(path); + File.WriteAllText(path + "/f1.txt", "foo"); + Directory.CreateDirectory(path + "/test"); + File.WriteAllText(path + "/test/f2.txt", "foo"); + Directory.CreateDirectory(path + "/test/inner"); + File.WriteAllText(path + "/test/inner/f3.txt", "foo"); + + path = NormPath(path); + var files = Directory.GetFiles(path); + Assert.AreEqual(1, files.Length); + files = Directory.GetFiles(path, "*", SearchOption.AllDirectories); + Assert.AreEqual(3, files.Length); + var efiles = Directory.EnumerateFiles(path); + Assert.AreEqual(1, efiles.Count()); + efiles = Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories); + Assert.AreEqual(3, efiles.Count()); + } + + [Test] + public void DeleteDirectoryAndFiles() + { + var path = IOHelper.MapPath("FileSysTests"); + Directory.CreateDirectory(path); + File.WriteAllText(path + "/f1.txt", "foo"); + Directory.CreateDirectory(path + "/test"); + File.WriteAllText(path + "/test/f2.txt", "foo"); + Directory.CreateDirectory(path + "/test/inner"); + File.WriteAllText(path + "/test/inner/f3.txt", "foo"); + + path = NormPath(path); + Directory.Delete(path, true); + + Assert.IsFalse(File.Exists(path + "/test/inner/f3.txt")); + } + } +} diff --git a/src/Umbraco.Tests/Logging/ConsoleLogger.cs b/src/Umbraco.Tests/Logging/ConsoleLogger.cs new file mode 100644 index 0000000000..05640e5541 --- /dev/null +++ b/src/Umbraco.Tests/Logging/ConsoleLogger.cs @@ -0,0 +1,107 @@ +using System; +using System.Linq; +using Umbraco.Core.Logging; + +// ReSharper disable LocalizableElement + +namespace Umbraco.Tests.Logging +{ + public class ConsoleLogger : ILogger + { + public void Error(Type reporting, string message, Exception exception = null) + { + WriteLine("WARN", reporting, message); + if (exception != null) + Console.WriteLine(exception); + } + + public void Warn(Type reporting, string message) + { + WriteLine("WARN", reporting, message); + } + + public void Warn(Type reporting, Func messageBuilder) + { + WriteLine("WARN", reporting, messageBuilder()); + } + + public void Warn(Type reporting, string format, params object[] args) + { + WriteLine("WARN", reporting, string.Format(format, args)); + } + + public void Warn(Type reporting, string format, params Func[] args) + { + WriteLine("WARN", reporting, string.Format(format, args.Select(x => x()))); + } + + public void Warn(Type reporting, Exception exception, string message) + { + Warn(reporting, message); + Console.WriteLine(exception); + } + + public void Warn(Type reporting, Exception exception, Func messageBuilder) + { + Warn(reporting, messageBuilder); + Console.WriteLine(exception); + } + + public void Warn(Type reporting, Exception exception, string format, params object[] args) + { + Warn(reporting, format, args); + Console.WriteLine(exception); + } + + public void Warn(Type reporting, Exception exception, string format, params Func[] args) + { + Warn(reporting, format, args); + Console.WriteLine(exception); + } + + public void Info(Type reporting, string message) + { + WriteLine("INFO", reporting, message); + } + + public void Info(Type reporting, Func messageBuilder) + { + WriteLine("INFO", reporting, messageBuilder()); + } + + public void Info(Type reporting, string format, params object[] args) + { + WriteLine("INFO", reporting, string.Format(format, args)); + } + + public void Info(Type reporting, string format, params Func[] args) + { + WriteLine("INFO", reporting, string.Format(format, args.Select(x => x()))); + } + + public void Debug(Type reporting, string message) + { + WriteLine("DEBUG", reporting, message); + } + + public void Debug(Type reporting, Func messageBuilder) + { + WriteLine("DEBUG", reporting, messageBuilder()); + } + + public void Debug(Type reporting, string format, params object[] args) + { + WriteLine("DEBUG", reporting, string.Format(format, args)); + } + + public void Debug(Type reporting, string format, params Func[] args) + { + WriteLine("DEBUG", reporting, string.Format(format, args.Select(x => x()))); + } + + private static void WriteLine(string level, Type reporting, string message) + { + Console.WriteLine("{0} {1} - {2}", level, reporting.Name, message); + } + } +} diff --git a/src/Umbraco.Tests/Migrations/MigrationIssuesTests.cs b/src/Umbraco.Tests/Migrations/MigrationIssuesTests.cs index f2bb1e2a8f..2a43a7511d 100644 --- a/src/Umbraco.Tests/Migrations/MigrationIssuesTests.cs +++ b/src/Umbraco.Tests/Migrations/MigrationIssuesTests.cs @@ -120,10 +120,7 @@ namespace Umbraco.Tests.Migrations //pass in explicit migrations new DeleteRedirectUrlTable(migrationContext), - new AddRedirectUrlTable(migrationContext), - new AddRedirectUrlTable2(migrationContext), - new AddRedirectUrlTable3(migrationContext), - new AddRedirectUrlTable4(migrationContext) + new AddRedirectUrlTable(migrationContext) ); var upgraded = migrationRunner.Execute(migrationContext, true); diff --git a/src/Umbraco.Tests/Migrations/Upgrades/SqlServerUpgradeTest.cs b/src/Umbraco.Tests/Migrations/Upgrades/SqlServerUpgradeTest.cs index 7f6e68d597..214f12134d 100644 --- a/src/Umbraco.Tests/Migrations/Upgrades/SqlServerUpgradeTest.cs +++ b/src/Umbraco.Tests/Migrations/Upgrades/SqlServerUpgradeTest.cs @@ -3,6 +3,7 @@ using System.Data.Common; using Moq; using NPoco; using NUnit.Framework; +using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.SqlSyntax; diff --git a/src/Umbraco.Tests/Models/ContentTests.cs b/src/Umbraco.Tests/Models/ContentTests.cs index 62b6da09ec..271d94db20 100644 --- a/src/Umbraco.Tests/Models/ContentTests.cs +++ b/src/Umbraco.Tests/Models/ContentTests.cs @@ -137,7 +137,7 @@ namespace Umbraco.Tests.Models var dataTypeService = Mock.Of(); // Assert - content.SetValue("title", postedFileMock.Object, dataTypeService); + content.SetValue("title", postedFileMock.Object); // Assert Assert.That(content.Properties.Any(), Is.True); diff --git a/src/Umbraco.Tests/Models/MediaXmlTest.cs b/src/Umbraco.Tests/Models/MediaXmlTest.cs index 98a5ebee42..e865e72818 100644 --- a/src/Umbraco.Tests/Models/MediaXmlTest.cs +++ b/src/Umbraco.Tests/Models/MediaXmlTest.cs @@ -1,11 +1,17 @@ using System.Linq; using System.Xml.Linq; +using Moq; using NUnit.Framework; using Umbraco.Core; +using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; using Umbraco.Core.Models; +using Umbraco.Core.Services; using Umbraco.Core.Strings; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; +using Umbraco.Web.PropertyEditors; namespace Umbraco.Tests.Models { @@ -20,9 +26,19 @@ namespace Umbraco.Tests.Models var mediaType = MockedContentTypes.CreateImageMediaType("image2"); ServiceContext.MediaTypeService.Save(mediaType); + // reference, so static ctor runs, so event handlers register + // and then, this will reset the width, height... because the file does not exist, of course ;-( + var ignored = new FileUploadPropertyEditor(Mock.Of(), new MediaFileSystem(Mock.Of()), Mock.Of(), Mock.Of()); + var media = MockedMedia.CreateMediaImage(mediaType, -1); ServiceContext.MediaService.Save(media, 0); + // so we have to force-reset these values because the property editor has cleared them + media.SetValue(Constants.Conventions.Media.Width, "200"); + media.SetValue(Constants.Conventions.Media.Height, "200"); + media.SetValue(Constants.Conventions.Media.Bytes, "100"); + media.SetValue(Constants.Conventions.Media.Extension, "png"); + var nodeName = media.ContentType.Alias.ToSafeAliasWithForcingCheck(); var urlName = media.GetUrlSegment(new[] { new DefaultUrlSegmentProvider() }); diff --git a/src/Umbraco.Tests/Persistence/NPocoExpressionsTests.cs b/src/Umbraco.Tests/Persistence/NPocoExpressionsTests.cs new file mode 100644 index 0000000000..d941f14e0b --- /dev/null +++ b/src/Umbraco.Tests/Persistence/NPocoExpressionsTests.cs @@ -0,0 +1,36 @@ +using NPoco; +using NUnit.Framework; +using Umbraco.Core.Models.Rdbms; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.SqlSyntax; +using Umbraco.Tests.TestHelpers; + +namespace Umbraco.Tests.Persistence +{ + [TestFixture] + public class NPocoExpressionsTests : BaseUsingSqlCeSyntax + { + [Test] + public void WhereInValueFieldTest() + { + var sql = new Sql(SqlContext) + .Select("*") + .From() + .WhereIn(x => x.NodeId, new[] { 1, 2, 3 }); + Assert.AreEqual("SELECT *\nFROM [umbracoNode]\nWHERE ([umbracoNode].[id] IN (@0,@1,@2))", sql.SQL); + } + + [Test] + public void WhereInObjectFieldTest() + { + // this test used to fail because x => x.Text was evaluated as a lambda + // and returned "[umbracoNode].[text] = @0"... had to fix WhereIn. + + var sql = new Sql(SqlContext) + .Select("*") + .From() + .WhereIn(x => x.Text, new[] { "a", "b", "c" }); + Assert.AreEqual("SELECT *\nFROM [umbracoNode]\nWHERE ([umbracoNode].[text] IN (@0,@1,@2))", sql.SQL); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Tests/Persistence/Querying/ContentRepositorySqlClausesTest.cs b/src/Umbraco.Tests/Persistence/Querying/ContentRepositorySqlClausesTest.cs index 87cab2acf0..03ae38f171 100644 --- a/src/Umbraco.Tests/Persistence/Querying/ContentRepositorySqlClausesTest.cs +++ b/src/Umbraco.Tests/Persistence/Querying/ContentRepositorySqlClausesTest.cs @@ -99,7 +99,7 @@ namespace Umbraco.Tests.Persistence.Querying .Where("([umbracoNode].[nodeObjectType] = @0)", new Guid("c66ba18e-eaf3-4cff-8a22-41b16d66a972")) .Where("([umbracoNode].[id] = @0)", 1050) .Where("([cmsContentVersion].[VersionId] = @0)", new Guid("2b543516-a944-4ee6-88c6-8813da7aaa07")) - .OrderBy("[cmsContentVersion].[VersionDate] DESC"); + .OrderBy("([cmsContentVersion].[VersionDate]) DESC"); var sql = Sql(); sql.SelectAll() diff --git a/src/Umbraco.Tests/Persistence/Querying/ExpressionTests.cs b/src/Umbraco.Tests/Persistence/Querying/ExpressionTests.cs index a8dd0b3707..69722c7621 100644 --- a/src/Umbraco.Tests/Persistence/Querying/ExpressionTests.cs +++ b/src/Umbraco.Tests/Persistence/Querying/ExpressionTests.cs @@ -102,8 +102,7 @@ namespace Umbraco.Tests.Persistence.Querying Debug.Print("Model to Sql ExpressionHelper: \n" + result); Assert.AreEqual("upper(`umbracoUser`.`userLogin`) = upper(@0)", result); - Assert.AreEqual("mydomain\\myuser", modelToSqlExpressionHelper.GetSqlParameters()[0]); - + Assert.AreEqual("mydomain\\myuser", modelToSqlExpressionHelper.GetSqlParameters()[0]); } [Test] @@ -123,5 +122,20 @@ namespace Umbraco.Tests.Persistence.Querying Assert.AreEqual("mydomain\\myuser%", modelToSqlExpressionHelper.GetSqlParameters()[0]); } + [Test] + public void Sql_Replace_Mapped() + { + Expression> predicate = user => user.Username.Replace("@world", "@test") == "hello@test.com"; + var modelToSqlExpressionHelper = new ModelToSqlExpressionHelper(SqlContext.SqlSyntax, new UserMapper()); + var result = modelToSqlExpressionHelper.Visit(predicate); + + Debug.Print("Model to Sql ExpressionHelper: \n" + result); + + Assert.AreEqual("(replace([umbracoUser].[userLogin], @1, @2) = @0)", result); + Assert.AreEqual("hello@test.com", modelToSqlExpressionHelper.GetSqlParameters()[0]); + Assert.AreEqual("@world", modelToSqlExpressionHelper.GetSqlParameters()[1]); + Assert.AreEqual("@test", modelToSqlExpressionHelper.GetSqlParameters()[2]); + } + } -} \ No newline at end of file +} diff --git a/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs b/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs index da3591eb84..631b67b85d 100644 --- a/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs +++ b/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs @@ -183,8 +183,17 @@ namespace Umbraco.Tests.PropertyEditors [Test] public void GetCropUrl_SpecifiedCropModeTest() { + var urlStringMin = MediaPath.GetCropUrl(imageCropperValue: CropperJson1, width: 300, height: 150, imageCropMode: ImageCropMode.Min); + var urlStringBoxPad = MediaPath.GetCropUrl(imageCropperValue: CropperJson1, width: 300, height: 150, imageCropMode: ImageCropMode.BoxPad); + var urlStringPad = MediaPath.GetCropUrl(imageCropperValue: CropperJson1, width: 300, height: 150, imageCropMode: ImageCropMode.Pad); var urlString = MediaPath.GetCropUrl(imageCropperValue: CropperJson1, width: 300, height: 150, imageCropMode:ImageCropMode.Max); + var urlStringStretch = MediaPath.GetCropUrl(imageCropperValue: CropperJson1, width: 300, height: 150, imageCropMode: ImageCropMode.Stretch); + + Assert.AreEqual(MediaPath + "?mode=min&width=300&height=150", urlStringMin); + Assert.AreEqual(MediaPath + "?mode=boxpad&width=300&height=150", urlStringBoxPad); + Assert.AreEqual(MediaPath + "?mode=pad&width=300&height=150", urlStringPad); Assert.AreEqual(MediaPath + "?mode=max&width=300&height=150", urlString); + Assert.AreEqual(MediaPath + "?mode=stretch&width=300&height=150", urlStringStretch); } /// diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentTestElements.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentTestElements.cs index 6c80fbde0f..6280cdabfa 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentTestElements.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentTestElements.cs @@ -74,6 +74,11 @@ namespace Umbraco.Tests.PublishedContent return _content.ContainsKey(contentId) ? _content[contentId] : null; } + public override IPublishedContent GetById(bool preview, Guid contentId) + { + throw new NotImplementedException(); + } + public override bool HasById(bool preview, int contentId) { return _content.ContainsKey(contentId); diff --git a/src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs b/src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs index b2dfac9ee5..26e7f8b070 100644 --- a/src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs +++ b/src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Web.Hosting; using LightInject; using Moq; using NUnit.Framework; @@ -121,7 +122,7 @@ namespace Umbraco.Tests.Runtimes public override void Terminate() { - _mainDom.Stop(false); + ((IRegisteredObject) _mainDom).Stop(false); base.Terminate(); } diff --git a/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs b/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs index 812f5ed5e4..c6bbc94368 100644 --- a/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs +++ b/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs @@ -7,7 +7,6 @@ using System.Threading.Tasks; using NUnit.Framework; using Umbraco.Core; using Umbraco.Core.Logging; -using Umbraco.Tests.TestHelpers; using Umbraco.Web.Scheduling; namespace Umbraco.Tests.Scheduling @@ -16,7 +15,7 @@ namespace Umbraco.Tests.Scheduling [Timeout(30000)] public class BackgroundTaskRunnerTests { - ILogger _logger; + private ILogger _logger; [TestFixtureSetUp] public void InitializeFixture() @@ -552,29 +551,29 @@ namespace Umbraco.Tests.Scheduling } [Test] - public async void DelayedTaskRuns() + public async void LatchedTaskRuns() { using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions(), _logger)) { - var task = new MyDelayedTask(200, false); + var task = new MyLatchedTask(200, false); runner.Add(task); Assert.IsTrue(runner.IsRunning); - Thread.Sleep(5000); + Thread.Sleep(1000); Assert.IsTrue(runner.IsRunning); // still waiting for the task to release Assert.IsFalse(task.HasRun); task.Release(); - await runner.CurrentThreadingTask; //wait for current task to complete + await runner.CurrentThreadingTask; // wait for current task to complete Assert.IsTrue(task.HasRun); await runner.StoppedAwaitable; // wait for the entire runner operation to complete } } [Test] - public async void DelayedTaskStops() + public async void LatchedTaskStops() { using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions(), _logger)) { - var task = new MyDelayedTask(200, true); + var task = new MyLatchedTask(200, true); runner.Add(task); Assert.IsTrue(runner.IsRunning); Thread.Sleep(5000); @@ -588,7 +587,7 @@ namespace Umbraco.Tests.Scheduling [Test] - public void DelayedRecurring() + public void LatchedRecurring() { var runCount = 0; var waitHandle = new ManualResetEvent(false); @@ -662,7 +661,6 @@ namespace Umbraco.Tests.Scheduling runner.Add(task); Assert.IsTrue(runner.IsRunning); await runner.StoppedAwaitable; // wait for the entire runner operation to complete - Assert.AreEqual(1, exceptions.Count); // traced and reported } } @@ -684,6 +682,38 @@ namespace Umbraco.Tests.Scheduling } } + [Test] + public async void CancelAsyncTask() + { + using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions(), _logger)) + { + var task = new MyAsyncTask(4000); + runner.Add(task); + Assert.IsTrue(runner.IsRunning); + await Task.Delay(1000); // ensure the task *has* started else cannot cancel + runner.CancelCurrentBackgroundTask(); + + await runner.StoppedAwaitable; // wait for the entire runner operation to complete + Assert.AreEqual(default(DateTime), task.Ended); + } + } + + [Test] + public async void CancelLatchedTask() + { + using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions(), _logger)) + { + var task = new MyLatchedTask(4000, false); + runner.Add(task); + Assert.IsTrue(runner.IsRunning); + await Task.Delay(1000); // ensure the task *has* started else cannot cancel + runner.CancelCurrentBackgroundTask(); + + await runner.StoppedAwaitable; // wait for the entire runner operation to complete + Assert.IsFalse(task.HasRun); + } + } + private class MyFailingTask : IBackgroundTask { private readonly bool _isAsync; @@ -706,7 +736,7 @@ namespace Umbraco.Tests.Scheduling public async Task RunAsync(CancellationToken token) { - await Task.Delay(1000); + await Task.Delay(1000, token); if (_running) throw new Exception("Task has thrown."); } @@ -750,28 +780,28 @@ namespace Umbraco.Tests.Scheduling public override bool RunsOnShutdown { get { return true; } } } - private class MyDelayedTask : ILatchedBackgroundTask + private class MyLatchedTask : ILatchedBackgroundTask { private readonly int _runMilliseconds; - private readonly ManualResetEventSlim _gate; + private readonly TaskCompletionSource _latch; public bool HasRun { get; private set; } - public MyDelayedTask(int runMilliseconds, bool runsOnShutdown) + public MyLatchedTask(int runMilliseconds, bool runsOnShutdown) { _runMilliseconds = runMilliseconds; - _gate = new ManualResetEventSlim(false); + _latch = new TaskCompletionSource(); RunsOnShutdown = runsOnShutdown; } - public WaitHandle Latch + public Task Latch { - get { return _gate.WaitHandle; } + get { return _latch.Task; } } public bool IsLatched { - get { return _gate.IsSet == false; } + get { return _latch.Task.IsCompleted == false; } } public bool RunsOnShutdown { get; private set; } @@ -784,7 +814,7 @@ namespace Umbraco.Tests.Scheduling public void Release() { - _gate.Set(); + _latch.SetResult(true); } public Task RunAsync(CancellationToken token) @@ -849,6 +879,86 @@ namespace Umbraco.Tests.Scheduling } } + private class MyAsyncTask : BaseTask + { + private readonly int _milliseconds; + + public MyAsyncTask() + : this(500) + { } + + public MyAsyncTask(int milliseconds) + { + _milliseconds = milliseconds; + } + + public override void PerformRun() + { + throw new NotImplementedException(); + } + + public override async Task RunAsync(CancellationToken token) + { + await Task.Delay(_milliseconds, token); + Ended = DateTime.Now; + } + + public override bool IsAsync + { + get { return true; } + } + } + + [Test] + public void SourceTaskTest() + { + var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions { KeepAlive = true, LongRunning = true }, _logger); + + var task = new SourceTask(); + runner.Add(task); + Assert.IsTrue(runner.IsRunning); + Console.WriteLine("completing"); + task.Complete(); // in Deploy this does not return ffs - no point until I cannot repro + Console.WriteLine("completed"); + Console.WriteLine("done"); + } + + private class SourceTask : IBackgroundTask + { + private readonly SemaphoreSlim _timeout = new SemaphoreSlim(0, 1); + private readonly TaskCompletionSource _source = new TaskCompletionSource(); + + public void Complete() + { + _source.SetResult(null); + } + + public void Dispose() + { } + + public void Run() + { + throw new NotImplementedException(); + } + + private int i; + + public async Task RunAsync(CancellationToken token) + { + Console.WriteLine("boom"); + var timeout = _timeout.WaitAsync(token); + var task = WorkItemRunAsync(); + var anyTask = await Task.WhenAny(task, timeout).ConfigureAwait(false); + } + + private async Task WorkItemRunAsync() + { + await _source.Task.ConfigureAwait(false); + } + + public bool IsAsync { get { return true; } } + } + public abstract class BaseTask : IBackgroundTask { public bool WasCancelled { get; set; } @@ -863,13 +973,13 @@ namespace Umbraco.Tests.Scheduling Ended = DateTime.Now; } - public Task RunAsync(CancellationToken token) + public virtual Task RunAsync(CancellationToken token) { throw new NotImplementedException(); //return Task.Delay(500); } - public bool IsAsync + public virtual bool IsAsync { get { return false; } } diff --git a/src/Umbraco.Tests/Scheduling/DeployTest.cs b/src/Umbraco.Tests/Scheduling/DeployTest.cs new file mode 100644 index 0000000000..60322ce7b7 --- /dev/null +++ b/src/Umbraco.Tests/Scheduling/DeployTest.cs @@ -0,0 +1,73 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using NUnit.Framework; +using Umbraco.Tests.Logging; +using Umbraco.Web.Scheduling; + +namespace Umbraco.Tests.Scheduling +{ + // THIS REPRODUCES THE DEPLOY ISSUE IN CORE + // + // the exact same thing also reproduces in playground + // so it's not a framework version issue - but something we're doing here + + [TestFixture] + [Timeout(4000)] + public class Repro + { + [Test] + public async Task ReproduceDeployIssue() + { + var logger = new ConsoleLogger(); + var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions { KeepAlive = true, LongRunning = true }, logger); + var work = new SimpleWorkItem(); + runner.Add(work); + + Console.WriteLine("running"); + await Task.Delay(1000); // don't complete too soon + + Console.WriteLine("completing"); + + // this never returns, never reached "completed" because the same thread + // resumes executing the waiting on queue operation in the runner + work.Complete(); + Console.WriteLine("completed"); + + Console.WriteLine("done"); + } + + public class SimpleWorkItem : IBackgroundTask + { + private TaskCompletionSource _completionSource; + + public async Task RunAsync(CancellationToken token) + { + _completionSource = new TaskCompletionSource(); + token.Register(() => _completionSource.TrySetCanceled()); // propagate + Console.WriteLine("item running..."); + await _completionSource.Task.ConfigureAwait(false); + Console.WriteLine("item returning"); + } + + public bool Complete(bool success = true) + { + Console.WriteLine("item completing"); + // this never returns, see test + _completionSource.SetResult(0); + Console.WriteLine("item returning from completing"); + return true; + } + + public void Run() + { + throw new NotImplementedException(); + } + + public bool IsAsync { get { return true; } } + + public void Dispose() + {} + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index cb84c21da3..d7262d940b 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -6,7 +6,6 @@ using System.Threading; using Moq; using NUnit.Framework; using Umbraco.Core.Configuration.UmbracoSettings; -using Umbraco.Core.DI; using Umbraco.Core.IO; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; @@ -18,6 +17,8 @@ using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; +using Umbraco.Core.DI; +using Umbraco.Core.Events; namespace Umbraco.Tests.Services { @@ -64,6 +65,50 @@ namespace Umbraco.Tests.Services Assert.IsTrue(contentService.PublishWithStatus(content).Success); } + [Test] + public void Get_Top_Version_Ids() + { + // Arrange + var contentService = ServiceContext.ContentService; + + // Act + var content = contentService.CreateContentWithIdentity("Test", -1, "umbTextpage", 0); + for (int i = 0; i < 20; i++) + { + content.SetValue("bodyText", "hello world " + Guid.NewGuid()); + contentService.SaveAndPublishWithStatus(content); + } + + + // Assert + var allVersions = contentService.GetVersionIds(content.Id, int.MaxValue); + Assert.AreEqual(21, allVersions.Count()); + + var topVersions = contentService.GetVersionIds(content.Id, 4); + Assert.AreEqual(4, topVersions.Count()); + } + + [Test] + public void Get_By_Ids_Sorted() + { + // Arrange + var contentService = ServiceContext.ContentService; + + // Act + var results = new List(); + for (int i = 0; i < 20; i++) + { + results.Add(contentService.CreateContentWithIdentity("Test", -1, "umbTextpage", 0)); + } + + var sortedGet = contentService.GetByIds(new[] {results[10].Id, results[5].Id, results[12].Id}).ToArray(); + + // Assert + Assert.AreEqual(sortedGet[0].Id, results[10].Id); + Assert.AreEqual(sortedGet[1].Id, results[5].Id); + Assert.AreEqual(sortedGet[2].Id, results[12].Id); + } + [Test] public void Count_All() { @@ -896,6 +941,46 @@ namespace Umbraco.Tests.Services Assert.That(content.Published, Is.True); } + [Test] + public void Can_Publish_Content_WithEvents() + { + ContentService.Publishing += ContentServiceOnPublishing; + + // tests that during 'publishing' event, what we get from the repo is the 'old' content, + // because 'publishing' fires before the 'saved' event ie before the content is actually + // saved + + try + { + var contentService = ServiceContext.ContentService; + var content = contentService.GetById(NodeDto.NodeIdSeed + 1); + Assert.AreEqual("Home", content.Name); + + content.Name = "foo"; + var published = contentService.Publish(content, 0); + + Assert.That(published, Is.True); + Assert.That(content.Published, Is.True); + + var e = ServiceContext.ContentService.GetById(content.Id); + Assert.AreEqual("foo", e.Name); + } + finally + { + ContentService.Publishing -= ContentServiceOnPublishing; + } + } + + private void ContentServiceOnPublishing(IContentService sender, PublishEventArgs args) + { + Assert.AreEqual(1, args.PublishedEntities.Count()); + var entity = args.PublishedEntities.First(); + Assert.AreEqual("foo", entity.Name); + + var e = ServiceContext.ContentService.GetById(entity.Id); + Assert.AreEqual("Home", e.Name); + } + [Test] public void Can_Publish_Only_Valid_Content() { diff --git a/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs b/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs index 8774bef524..bba2d54bd3 100644 --- a/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs @@ -263,6 +263,35 @@ namespace Umbraco.Tests.Services Assert.That(success, Is.False); } + [Test] + public void Can_Delete_Parent_ContentType_When_Child_Has_Content() + { + var cts = ServiceContext.ContentTypeService; + var contentType = MockedContentTypes.CreateSimpleContentType("page", "Page", null, true); + cts.Save(contentType); + var childContentType = MockedContentTypes.CreateSimpleContentType("childPage", "Child Page", contentType, true, "Child Content"); + cts.Save(childContentType); + var cs = ServiceContext.ContentService; + var content = cs.CreateContent("Page 1", -1, childContentType.Alias); + cs.Save(content); + + cts.Delete(contentType); + + Assert.IsNotNull(content.Id); + Assert.AreNotEqual(0, content.Id); + Assert.IsNotNull(childContentType.Id); + Assert.AreNotEqual(0, childContentType.Id); + Assert.IsNotNull(contentType.Id); + Assert.AreNotEqual(0, contentType.Id); + var deletedContent = cs.GetById(content.Id); + var deletedChildContentType = cts.Get(childContentType.Id); + var deletedContentType = cts.Get(contentType.Id); + + Assert.IsNull(deletedChildContentType); + Assert.IsNull(deletedContent); + Assert.IsNull(deletedContentType); + } + [Test] public void Deleting_ContentType_Sends_Correct_Number_Of_DeletedEntities_In_Events() { diff --git a/src/Umbraco.Tests/Services/MemberServiceTests.cs b/src/Umbraco.Tests/Services/MemberServiceTests.cs index 13de4c3952..5f6c2fe458 100644 --- a/src/Umbraco.Tests/Services/MemberServiceTests.cs +++ b/src/Umbraco.Tests/Services/MemberServiceTests.cs @@ -9,6 +9,7 @@ using Umbraco.Core.Models.EntityBase; 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; @@ -23,6 +24,34 @@ namespace Umbraco.Tests.Services [TestSetup.FacadeService(EnableRepositoryEvents = true)] public class MemberServiceTests : BaseServiceTest { + [Test] + public void Can_Create_Member() + { + IMemberType memberType = MockedContentTypes.CreateSimpleMemberType(); + ServiceContext.MemberTypeService.Save(memberType); + IMember member = MockedMember.CreateSimpleMember(memberType, "test", "test@test.com", "pass", "test"); + ServiceContext.MemberService.Save(member); + + Assert.AreNotEqual(0, member.Id); + var foundMember = ServiceContext.MemberService.GetById(member.Id); + Assert.IsNotNull(foundMember); + Assert.AreEqual("test@test.com", foundMember.Email); + } + + [Test] + public void Can_Create_Member_With_Long_TLD_In_Email() + { + IMemberType memberType = MockedContentTypes.CreateSimpleMemberType(); + ServiceContext.MemberTypeService.Save(memberType); + IMember member = MockedMember.CreateSimpleMember(memberType, "test", "test@test.marketing", "pass", "test"); + ServiceContext.MemberService.Save(member); + + Assert.AreNotEqual(0, member.Id); + var foundMember = ServiceContext.MemberService.GetById(member.Id); + Assert.IsNotNull(foundMember); + Assert.AreEqual("test@test.marketing", foundMember.Email); + } + [Test] public void Can_Create_Role() { diff --git a/src/Umbraco.Tests/TestHelpers/TestHelper.cs b/src/Umbraco.Tests/TestHelpers/TestHelper.cs index 85d57c8886..42c10d6623 100644 --- a/src/Umbraco.Tests/TestHelpers/TestHelper.cs +++ b/src/Umbraco.Tests/TestHelpers/TestHelper.cs @@ -141,7 +141,13 @@ namespace Umbraco.Tests.TestHelpers } else if (dateTimeFormat.IsNullOrWhiteSpace() == false && actualValue is DateTime) { - Assert.AreEqual(((DateTime) expectedValue).ToString(dateTimeFormat), ((DateTime)actualValue).ToString(dateTimeFormat), "Property {0}.{1} does not match. Expected: {2} but was: {3}", property.DeclaringType.Name, property.Name, expectedValue, actualValue); + // round to second else in some cases tests can fail ;-( + var expectedDateTime = (DateTime) expectedValue; + expectedDateTime = expectedDateTime.AddTicks(-(expectedDateTime.Ticks%TimeSpan.TicksPerSecond)); + var actualDateTime = (DateTime) actualValue; + actualDateTime = actualDateTime.AddTicks(-(actualDateTime.Ticks % TimeSpan.TicksPerSecond)); + + Assert.AreEqual(expectedDateTime.ToString(dateTimeFormat), actualDateTime.ToString(dateTimeFormat), "Property {0}.{1} does not match. Expected: {2} but was: {3}", property.DeclaringType.Name, property.Name, expectedValue, actualValue); } else { diff --git a/src/Umbraco.Tests/TestHelpers/TestObjects.cs b/src/Umbraco.Tests/TestHelpers/TestObjects.cs index e7f4c8a2b2..e699887c07 100644 --- a/src/Umbraco.Tests/TestHelpers/TestObjects.cs +++ b/src/Umbraco.Tests/TestHelpers/TestObjects.cs @@ -104,6 +104,7 @@ namespace Umbraco.Tests.TestHelpers var provider = dbUnitOfWorkProvider; var fileProvider = fileUnitOfWorkProvider; + var mediaFileSystem = new MediaFileSystem(Mock.Of()); var migrationEntryService = GetLazyService(container, () => new MigrationEntryService(provider, logger, eventMessagesFactory)); var externalLoginService = GetLazyService(container, () => new ExternalLoginService(provider, logger, eventMessagesFactory)); @@ -146,12 +147,12 @@ namespace Umbraco.Tests.TestHelpers var userService = GetLazyService(container, () => new UserService(provider, logger, eventMessagesFactory)); var dataTypeService = GetLazyService(container, () => new DataTypeService(provider, logger, eventMessagesFactory)); - var contentService = GetLazyService(container, () => new ContentService(provider, logger, eventMessagesFactory)); + var contentService = GetLazyService(container, () => new ContentService(provider, logger, eventMessagesFactory, mediaFileSystem)); var notificationService = GetLazyService(container, () => new NotificationService(provider, userService.Value, contentService.Value, logger)); var serverRegistrationService = GetLazyService(container, () => new ServerRegistrationService(provider, logger, eventMessagesFactory)); var memberGroupService = GetLazyService(container, () => new MemberGroupService(provider, logger, eventMessagesFactory)); - var memberService = GetLazyService(container, () => new MemberService(provider, logger, eventMessagesFactory, memberGroupService.Value)); - var mediaService = GetLazyService(container, () => new MediaService(provider, logger, eventMessagesFactory)); + var memberService = GetLazyService(container, () => new MemberService(provider, logger, eventMessagesFactory, memberGroupService.Value, mediaFileSystem)); + var mediaService = GetLazyService(container, () => new MediaService(provider, mediaFileSystem, logger, eventMessagesFactory)); var contentTypeService = GetLazyService(container, () => new ContentTypeService(provider, logger, eventMessagesFactory, contentService.Value)); var mediaTypeService = GetLazyService(container, () => new MediaTypeService(provider, logger, eventMessagesFactory, mediaService.Value)); var fileService = GetLazyService(container, () => new FileService(fileProvider, provider, logger, eventMessagesFactory)); diff --git a/src/Umbraco.Tests/TestHelpers/TestWithApplicationBase.cs b/src/Umbraco.Tests/TestHelpers/TestWithApplicationBase.cs index 7cd369925e..2eeec76cc1 100644 --- a/src/Umbraco.Tests/TestHelpers/TestWithApplicationBase.cs +++ b/src/Umbraco.Tests/TestHelpers/TestWithApplicationBase.cs @@ -129,7 +129,7 @@ namespace Umbraco.Tests.TestHelpers Container.RegisterSingleton(factory => settings.Content); Container.RegisterSingleton(factory => settings.Templates); Container.Register(); - Container.Register(factory => new MediaFileSystem(Mock.Of())); + Container.Register(factory => new MediaFileSystem(Mock.Of())); Container.RegisterSingleton(); // replace some stuff diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 26921a97a9..db710d6698 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -227,15 +227,20 @@ + + + + + diff --git a/src/Umbraco.Web.UI.Client/bower.json b/src/Umbraco.Web.UI.Client/bower.json index 22ddfc2ee7..2fbfd57fe5 100644 --- a/src/Umbraco.Web.UI.Client/bower.json +++ b/src/Umbraco.Web.UI.Client/bower.json @@ -21,8 +21,9 @@ "underscore": "~1.7.0", "rgrove-lazyload": "*", "bootstrap-social": "~4.8.0", - "jquery": "2.0.3", + "jquery": "2.2.4", "jquery-ui": "1.11.4", + "jquery-migrate": "1.4.0", "angular-dynamic-locale": "0.1.28", "ng-file-upload": "~7.3.8", "tinymce": "~4.1.10", diff --git a/src/Umbraco.Web.UI.Client/gruntFile.js b/src/Umbraco.Web.UI.Client/gruntFile.js index d5b785c54c..0a28d9efaf 100644 --- a/src/Umbraco.Web.UI.Client/gruntFile.js +++ b/src/Umbraco.Web.UI.Client/gruntFile.js @@ -484,12 +484,17 @@ module.exports = function (grunt) { files: ['css/font-awesome.min.css', 'fonts/*'] }, "jquery": { - files: ['jquery.min.js', 'jquery.min.map'] + keepExpandedHierarchy: false, + files: ['dist/jquery.min.js', 'dist/jquery.min.map'] }, 'jquery-ui': { keepExpandedHierarchy: false, files: ['jquery-ui.min.js'] }, + 'jquery-migrate': { + keepExpandedHierarchy: false, + files: ['jquery-migrate.min.js'] + }, 'tinymce': { files: ['plugins/**', 'themes/**', 'tinymce.min.js'] }, diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js index 0692a10e90..5dd7d266df 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js @@ -64,6 +64,7 @@ angular.module("umbraco.directives") await.push(stylesheetResource.getRulesByName(stylesheet).then(function (rules) { angular.forEach(rules, function (rule) { var r = {}; + var split = ""; r.title = rule.name; if (rule.selector[0] === ".") { r.inline = "span"; @@ -74,6 +75,14 @@ angular.module("umbraco.directives") // since only one element can have one id. r.inline = "span"; r.attributes = { id: rule.selector.substring(1) }; + }else if (rule.selector[0] !== "." && rule.selector.indexOf(".") > -1) { + split = rule.selector.split("."); + r.block = split[0]; + r.classes = rule.selector.substring(rule.selector.indexOf(".") + 1).replace(".", " "); + }else if (rule.selector[0] !== "#" && rule.selector.indexOf("#") > -1) { + split = rule.selector.split("#"); + r.block = split[0]; + r.classes = rule.selector.substring(rule.selector.indexOf("#") + 1); }else { r.block = rule.selector; } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagegravity.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagegravity.directive.js index 2a183564f6..836d998412 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagegravity.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagegravity.directive.js @@ -118,9 +118,13 @@ angular.module("umbraco.directives") $timeout(function(){ setDimensions(); }); - var offsetX = $overlay[0].offsetLeft; - var offsetY = $overlay[0].offsetTop; - calculateGravity(offsetX, offsetY); + // Make sure we can find the offset values for the overlay(dot) before calculating + // fixes issue with resize event when printing the page (ex. hitting ctrl+p inside the rte) + if($overlay.is(':visible')) { + var offsetX = $overlay[0].offsetLeft; + var offsetY = $overlay[0].offsetTop; + calculateGravity(offsetX, offsetY); + } }); }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/tabs/umbtabs.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tabs/umbtabs.directive.js index 882372aba9..aa23b80665 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/tabs/umbtabs.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tabs/umbtabs.directive.js @@ -20,6 +20,8 @@ angular.module("umbraco.directives") var curr = $(event.target); // active tab var prev = $(event.relatedTarget); // previous tab + $scope.$apply(); + for (var c in callbacks) { callbacks[c].apply(this, [{current: curr, previous: prev}]); } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreesearchbox.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreesearchbox.directive.js index a3b2fab00f..4ba4cf96bb 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreesearchbox.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreesearchbox.directive.js @@ -43,7 +43,6 @@ function treeSearchBox(localizationService, searchService, $q) { //a canceler exists, so perform the cancelation operation and reset if (canceler) { - console.log("CANCELED!"); canceler.resolve(); canceler = $q.defer(); } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umblistviewsettings.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umblistviewsettings.directive.js index ae9da02237..f21f7b8d3d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umblistviewsettings.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umblistviewsettings.directive.js @@ -1,7 +1,7 @@ (function() { 'use strict'; - function ListViewSettingsDirective(contentTypeResource, dataTypeResource, dataTypeHelper) { + function ListViewSettingsDirective(contentTypeResource, dataTypeResource, dataTypeHelper, listViewPrevalueHelper) { function link(scope, el, attr, ctrl) { @@ -20,6 +20,7 @@ scope.dataType = dataType; + listViewPrevalueHelper.setPrevalues(dataType.preValues); scope.customListViewCreated = checkForCustomListView(); }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbmediagrid.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbmediagrid.directive.js index 7d2da34988..e9e7395761 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbmediagrid.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbmediagrid.directive.js @@ -138,6 +138,22 @@ Use this directive to generate a thumbnail grid of media items. if (!item.isFolder) { item.thumbnail = mediaHelper.resolveFile(item, true); item.image = mediaHelper.resolveFile(item, false); + + var fileProp = _.find(item.properties, function (v) { + return (v.alias === "umbracoFile"); + }); + + if (fileProp && fileProp.value) { + item.file = fileProp.value; + } + + var extensionProp = _.find(item.properties, function (v) { + return (v.alias === "umbracoExtension"); + }); + + if (extensionProp && extensionProp.value) { + item.extension = extensionProp.value; + } } } diff --git a/src/Umbraco.Web.UI.Client/src/common/mocks/editors/prevalues.mocks.js b/src/Umbraco.Web.UI.Client/src/common/mocks/editors/prevalues.mocks.js index d181d3d25a..a2d0a6fa3b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/mocks/editors/prevalues.mocks.js +++ b/src/Umbraco.Web.UI.Client/src/common/mocks/editors/prevalues.mocks.js @@ -58,7 +58,7 @@ angular.module('umbraco.mocks'). "view": "textstring", "icon": "icon-quote", "config": { - "style": "border-left: 3px solid #ccc; padding: 10px; color: #ccc; font-family: serif; font-variant: italic; font-size: 18px", + "style": "border-left: 3px solid #ccc; padding: 10px; color: #ccc; font-family: serif; font-style: italic; font-size: 18px", "markup": "
#value#
" } } diff --git a/src/Umbraco.Web.UI.Client/src/common/mocks/services/localization.mocks.js b/src/Umbraco.Web.UI.Client/src/common/mocks/services/localization.mocks.js index 086615ae5f..7c75dc2abb 100644 --- a/src/Umbraco.Web.UI.Client/src/common/mocks/services/localization.mocks.js +++ b/src/Umbraco.Web.UI.Client/src/common/mocks/services/localization.mocks.js @@ -134,7 +134,7 @@ angular.module('umbraco.mocks'). "content_nodeName": "Page Title", "content_otherElements": "Properties", "content_parentNotPublished": "This document is published but is not visible because the parent '%0%' is unpublished", - "content_parentNotPublishedAnomaly": "Oops: this document is published but is not in the cache (internal error)", + "content_parentNotPublishedAnomaly": "This document is published but is not in the cache", "content_publish": "Publish", "content_publishStatus": "Publication Status", "content_releaseDate": "Publish at", diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/dashboard.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/dashboard.resource.js index ca3ae03876..c48b2dd2a7 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/dashboard.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/dashboard.resource.js @@ -20,7 +20,6 @@ function dashboardResource($q, $http, umbRequestHelper) { * */ getDashboard: function (section) { - return umbRequestHelper.resourcePromise( $http.get( umbRequestHelper.getApiUrl( @@ -28,7 +27,53 @@ function dashboardResource($q, $http, umbRequestHelper) { "GetDashboard", [{ section: section }])), 'Failed to get dashboard ' + section); + }, + + /** + * @ngdoc method + * @name umbraco.resources.dashboardResource#getRemoteDashboardContent + * @methodOf umbraco.resources.dashboardResource + * + * @description + * Retrieves dashboard content from a remote source for a given section + * + * @param {string} section Alias of section to retrieve dashboard content for + * @returns {Promise} resourcePromise object containing the user array. + * + */ + getRemoteDashboardContent: function (section, baseurl) { + + //build request values with optional params + var values = [{ section: section }]; + if (baseurl) + { + values.push({ baseurl: baseurl }); + } + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "dashboardApiBaseUrl", + "GetRemoteDashboardContent", + values)), "Failed to get dashboard content"); + }, + + getRemoteDashboardCssUrl: function (section, baseurl) { + + //build request values with optional params + var values = [{ section: section }]; + if (baseurl) { + values.push({ baseurl: baseurl }); + } + + return umbRequestHelper.getApiUrl( + "dashboardApiBaseUrl", + "GetRemoteDashboardCss", + values); } + + + }; } diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js index a9296acc37..914b601249 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js @@ -109,16 +109,6 @@ function entityResource($q, $http, umbRequestHelper) { [{ id: id}, {type: type }])), 'Failed to retrieve entity data for id ' + id); }, - - getByQuery: function (query, nodeContextId, type) { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "entityApiBaseUrl", - "GetByQuery", - [{query: query},{ nodeContextId: nodeContextId}, {type: type }])), - 'Failed to retrieve entity data for query ' + query); - }, /** * @ngdoc method @@ -168,7 +158,41 @@ function entityResource($q, $http, umbRequestHelper) { /** * @ngdoc method - * @name umbraco.resources.entityResource#getEntityById + * @name umbraco.resources.entityResource#getByQuery + * @methodOf umbraco.resources.entityResource + * + * @description + * Gets an entity from a given xpath + * + * ##usage + *
+         * //get content by xpath
+         * entityResource.getByQuery("$current", -1, "Document")
+         *    .then(function(ent) {
+         *        var myDoc = ent; 
+         *        alert('its here!');
+         *    });
+         * 
+ * + * @param {string} query xpath to use in query + * @param {Int} nodeContextId id id to start from + * @param {string} type Object type name + * @returns {Promise} resourcePromise object containing the entity. + * + */ + getByQuery: function (query, nodeContextId, type) { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "entityApiBaseUrl", + "GetByQuery", + [{ query: query }, { nodeContextId: nodeContextId }, { type: type }])), + 'Failed to retrieve entity data for query ' + query); + }, + + /** + * @ngdoc method + * @name umbraco.resources.entityResource#getAll * @methodOf umbraco.resources.entityResource * * @description @@ -260,7 +284,7 @@ function entityResource($q, $http, umbRequestHelper) { /** * @ngdoc method - * @name umbraco.resources.entityResource#searchMedia + * @name umbraco.resources.entityResource#search * @methodOf umbraco.resources.entityResource * * @description diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/ourpackagerrepository.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/ourpackagerrepository.resource.js index d9307c7c28..053aaf1394 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/ourpackagerrepository.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/ourpackagerrepository.resource.js @@ -5,6 +5,7 @@ **/ function ourPackageRepositoryResource($q, $http, umbDataFormatter, umbRequestHelper) { + //var baseurl = "http://localhost:24292/webapi/packages/v1"; var baseurl = "https://our.umbraco.org/webapi/packages/v1"; return { @@ -33,17 +34,17 @@ function ourPackageRepositoryResource($q, $http, umbDataFormatter, umbRequestHel } return umbRequestHelper.resourcePromise( - $http.get(baseurl + "?pageIndex=0&pageSize=" + maxResults + "&category=" + category + "&order=Popular"), + $http.get(baseurl + "?pageIndex=0&pageSize=" + maxResults + "&category=" + category + "&order=Popular&version=" + Umbraco.Sys.ServerVariables.application.version), 'Failed to query packages'); }, - search: function (pageIndex, pageSize, category, query, canceler) { + search: function (pageIndex, pageSize, orderBy, category, query, canceler) { var httpConfig = {}; if (canceler) { httpConfig["timeout"] = canceler; } - + if (category === undefined) { category = ""; } @@ -51,8 +52,11 @@ function ourPackageRepositoryResource($q, $http, umbDataFormatter, umbRequestHel query = ""; } + //order by score if there is nothing set + var order = !orderBy ? "&order=Default" : ("&order=" + orderBy); + return umbRequestHelper.resourcePromise( - $http.get(baseurl + "?pageIndex=" + pageIndex + "&pageSize=" + pageSize + "&category=" + category + "&query=" + query), + $http.get(baseurl + "?pageIndex=" + pageIndex + "&pageSize=" + pageSize + "&category=" + category + "&query=" + query + order + "&version=" + Umbraco.Sys.ServerVariables.application.version), httpConfig, 'Failed to query packages'); } diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/package.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/package.resource.js index 9dae2008e2..c9a501ba24 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/package.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/package.resource.js @@ -111,7 +111,7 @@ function packageResource($q, $http, umbDataFormatter, umbRequestHelper) { umbRequestHelper.getApiUrl( "packageInstallApiBaseUrl", "Import"), package), - 'Failed to create package manifest for zip file '); + 'Failed to install package. Error during the step "Import" '); }, installFiles: function (package) { @@ -120,7 +120,7 @@ function packageResource($q, $http, umbDataFormatter, umbRequestHelper) { umbRequestHelper.getApiUrl( "packageInstallApiBaseUrl", "InstallFiles"), package), - 'Failed to create package manifest for zip file '); + 'Failed to install package. Error during the step "InstallFiles" '); }, installData: function (package) { @@ -130,7 +130,7 @@ function packageResource($q, $http, umbDataFormatter, umbRequestHelper) { umbRequestHelper.getApiUrl( "packageInstallApiBaseUrl", "InstallData"), package), - 'Failed to create package manifest for zip file '); + 'Failed to install package. Error during the step "InstallData" '); }, cleanUp: function (package) { @@ -140,7 +140,7 @@ function packageResource($q, $http, umbDataFormatter, umbRequestHelper) { umbRequestHelper.getApiUrl( "packageInstallApiBaseUrl", "CleanUp"), package), - 'Failed to create package manifest for zip file '); + 'Failed to install package. Error during the step "CleanUp" '); } }; } diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/redirecturls.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/redirecturls.resource.js new file mode 100644 index 0000000000..ea40c066f0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/resources/redirecturls.resource.js @@ -0,0 +1,119 @@ +/** + * @ngdoc service + * @name umbraco.resources.redirectUrlResource + * @function + * + * @description + * Used by the redirect url dashboard to get urls and send requests to remove redirects. + */ +(function() { + 'use strict'; + + function redirectUrlsResource($http, umbRequestHelper) { + + /** + * @ngdoc function + * @name umbraco.resources.redirectUrlResource#searchRedirectUrls + * @methodOf umbraco.resources.redirectUrlResource + * @function + * + * @description + * Called to search redirects + * ##usage + *
+         * redirectUrlsResource.searchRedirectUrls("", 0, 20)
+         *    .then(function(response) {
+         *
+         *    });
+         * 
+ * @param {String} searchTerm Searh term + * @param {Int} pageIndex index of the page to retrive items from + * @param {Int} pageSize The number of items on a page + */ + function searchRedirectUrls(searchTerm, pageIndex, pageSize) { + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "redirectUrlManagementApiBaseUrl", + "SearchRedirectUrls", + { searchTerm: searchTerm, page: pageIndex, pageSize: pageSize })), + 'Failed to retrieve data for searching redirect urls'); + } + + function getEnableState() { + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "redirectUrlManagementApiBaseUrl", + "GetEnableState")), + 'Failed to retrieve data to check if the 301 redirect is enabled'); + } + + /** + * @ngdoc function + * @name umbraco.resources.redirectUrlResource#deleteRedirectUrl + * @methodOf umbraco.resources.redirectUrlResource + * @function + * + * @description + * Called to delete a redirect + * ##usage + *
+         * redirectUrlsResource.deleteRedirectUrl(1234)
+         *    .then(function() {
+         *
+         *    });
+         * 
+ * @param {Int} id Id of the redirect + */ + function deleteRedirectUrl(id) { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "redirectUrlManagementApiBaseUrl", + "DeleteRedirectUrl", { id: id })), + 'Failed to remove redirect'); + } + + /** + * @ngdoc function + * @name umbraco.resources.redirectUrlResource#toggleUrlTracker + * @methodOf umbraco.resources.redirectUrlResource + * @function + * + * @description + * Called to enable or disable redirect url tracker + * ##usage + *
+         * redirectUrlsResource.toggleUrlTracker(true)
+         *    .then(function() {
+         *
+         *    });
+         * 
+ * @param {Bool} disable true/false to disable/enable the url tracker + */ + function toggleUrlTracker(disable) { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "redirectUrlManagementApiBaseUrl", + "ToggleUrlTracker", { disable: disable })), + 'Failed to toggle redirect url tracker'); + } + + var resource = { + searchRedirectUrls: searchRedirectUrls, + deleteRedirectUrl: deleteRedirectUrl, + toggleUrlTracker: toggleUrlTracker, + getEnableState: getEnableState + }; + + return resource; + + } + + angular.module('umbraco.resources').factory('redirectUrlsResource', redirectUrlsResource); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/security/retryqueue.js b/src/Umbraco.Web.UI.Client/src/common/security/retryqueue.js index 28d91dd610..755e179527 100644 --- a/src/Umbraco.Web.UI.Client/src/common/security/retryqueue.js +++ b/src/Umbraco.Web.UI.Client/src/common/security/retryqueue.js @@ -5,10 +5,12 @@ angular.module('umbraco.security.retryQueue', []) .factory('securityRetryQueue', ['$q', '$log', function ($q, $log) { var retryQueue = []; + var retryUser = null; + var service = { // The security service puts its own handler in here! onItemAddedCallbacks: [], - + hasMore: function() { return retryQueue.length > 0; }, @@ -23,13 +25,20 @@ angular.module('umbraco.security.retryQueue', []) } }); }, - pushRetryFn: function(reason, retryFn) { + pushRetryFn: function(reason, userName, retryFn) { // The reason parameter is optional - if ( arguments.length === 1) { - retryFn = reason; + if ( arguments.length === 2) { + retryFn = userName; + userName = reason; reason = undefined; } + if ((retryUser && retryUser !== userName) || userName === null) { + throw new Error('invalid user'); + } + + retryUser = userName; + // The deferred object that will be resolved or rejected by calling retry or cancel var deferred = $q.defer(); var retryItem = { @@ -59,8 +68,19 @@ angular.module('umbraco.security.retryQueue', []) while(service.hasMore()) { retryQueue.shift().cancel(); } + retryUser = null; }, - retryAll: function() { + retryAll: function (userName) { + + if (retryUser == null) { + return; + } + + if (retryUser !== userName) { + service.cancelAll(); + return; + } + while(service.hasMore()) { retryQueue.shift().retry(); } diff --git a/src/Umbraco.Web.UI.Client/src/common/security/securityinterceptor.js b/src/Umbraco.Web.UI.Client/src/common/security/securityinterceptor.js index b80754ef67..e47f0663d8 100644 --- a/src/Umbraco.Web.UI.Client/src/common/security/securityinterceptor.js +++ b/src/Umbraco.Web.UI.Client/src/common/security/securityinterceptor.js @@ -6,7 +6,7 @@ angular.module('umbraco.security.interceptor') return promise.then( function(originalResponse) { // Intercept successful requests - + //Here we'll check if our custom header is in the response which indicates how many seconds the user's session has before it //expires. Then we'll update the user in the user service accordingly. var headers = originalResponse.headers(); @@ -15,11 +15,11 @@ angular.module('umbraco.security.interceptor') var userService = $injector.get('userService'); userService.setUserTimeout(headers["x-umb-user-seconds"]); } - + return promise; }, function(originalResponse) { // Intercept failed requests - + //Here we'll check if we should ignore the error, this will be based on an original header set var headers = originalResponse.config ? originalResponse.config.headers : {}; if (headers["x-umb-ignore-error"] === "ignore") { @@ -36,17 +36,24 @@ angular.module('umbraco.security.interceptor') //A 401 means that the user is not logged in if (originalResponse.status === 401) { - // The request bounced because it was not authorized - add a new request to the retry queue - promise = queue.pushRetryFn('unauthorized-server', function retryRequest() { + var userService = $injector.get('userService'); // see above + + //Associate the user name with the retry to ensure we retry for the right user + promise = userService.getCurrentUser() + .then(function (user) { + var userName = user ? user.name : null; + //The request bounced because it was not authorized - add a new request to the retry queue + return queue.pushRetryFn('unauthorized-server', userName, function retryRequest() { // We must use $injector to get the $http service to prevent circular dependency return $injector.get('$http')(originalResponse.config); + }); }); } else if (originalResponse.status === 404) { //a 404 indicates that the request was not found - this could be due to a non existing url, or it could //be due to accessing a url with a parameter that doesn't exist, either way we should notifiy the user about it - + var errMsg = "The URL returned a 404 (not found):
" + originalResponse.config.url.split('?')[0] + ""; if (originalResponse.data && originalResponse.data.ExceptionMessage) { errMsg += "
with error:
" + originalResponse.data.ExceptionMessage + ""; @@ -58,17 +65,17 @@ angular.module('umbraco.security.interceptor') notifications.error( "Request error", errMsg); - + } else if (originalResponse.status === 403) { //if the status was a 403 it means the user didn't have permission to do what the request was trying to do. - //How do we deal with this now, need to tell the user somehow that they don't have permission to do the thing that was + //How do we deal with this now, need to tell the user somehow that they don't have permission to do the thing that was //requested. We can either deal with this globally here, or we can deal with it globally for individual requests on the umbRequestHelper, // or completely custom for services calling resources. //http://issues.umbraco.org/issue/U4-2749 - //It was decided to just put these messages into the normal status messages. + //It was decided to just put these messages into the normal status messages. var msg = "Unauthorized access to URL:
" + originalResponse.config.url.split('?')[0] + ""; if (originalResponse.config.data) { diff --git a/src/Umbraco.Web.UI.Client/src/common/services/dialog.service.js b/src/Umbraco.Web.UI.Client/src/common/services/dialog.service.js index 263397787d..6364cd8054 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/dialog.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/dialog.service.js @@ -354,7 +354,7 @@ angular.module('umbraco.services') * @description * Opens a content picker tree in a modal, the callback returns an array of selected documents * @param {Object} options content picker dialog options object - * @param {Boolean} options.multipicker should the picker return one or multiple items + * @param {Boolean} options.multiPicker should the picker return one or multiple items * @param {Function} options.callback callback function * @returns {Object} modal object */ diff --git a/src/Umbraco.Web.UI.Client/src/common/services/listviewprevaluehelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/listviewprevaluehelper.service.js new file mode 100644 index 0000000000..70bba2d26a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/services/listviewprevaluehelper.service.js @@ -0,0 +1,61 @@ +/** + @ngdoc service + * @name umbraco.services.listViewPrevalueHelper + * + * + * @description + * Service for accessing the prevalues of a list view being edited in the inline list view editor in the doctype editor + */ +(function () { + 'use strict'; + + function listViewPrevalueHelper() { + + var prevalues = []; + + /** + * @ngdoc method + * @name umbraco.services.listViewPrevalueHelper#getPrevalues + * @methodOf umbraco.services.listViewPrevalueHelper + * + * @description + * Set the collection of prevalues + */ + + function getPrevalues() { + return prevalues; + } + + /** + * @ngdoc method + * @name umbraco.services.listViewPrevalueHelper#setPrevalues + * @methodOf umbraco.services.listViewPrevalueHelper + * + * @description + * Changes the current layout used by the listview to the layout passed in. Stores selection in localstorage + * + * @param {Array} values Array of prevalues + */ + + function setPrevalues(values) { + prevalues = values; + } + + + + var service = { + + getPrevalues: getPrevalues, + setPrevalues: setPrevalues + + }; + + return service; + + } + + + angular.module('umbraco.services').factory('listViewPrevalueHelper', listViewPrevalueHelper); + + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/user.service.js b/src/Umbraco.Web.UI.Client/src/common/services/user.service.js index dffda24f1d..c759169752 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/user.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/user.service.js @@ -31,7 +31,7 @@ angular.module('umbraco.services') loginDialog = null; if (success) { - securityRetryQueue.retryAll(); + securityRetryQueue.retryAll(currentUser.name); } else { securityRetryQueue.cancelAll(); @@ -39,9 +39,9 @@ angular.module('umbraco.services') } } - /** - This methods will set the current user when it is resolved and - will then start the counter to count in-memory how many seconds they have + /** + This methods will set the current user when it is resolved and + will then start the counter to count in-memory how many seconds they have remaining on the auth session */ function setCurrentUser(usr) { @@ -54,8 +54,8 @@ angular.module('umbraco.services') countdownUserTimeout(); } - /** - Method to count down the current user's timeout seconds, + /** + Method to count down the current user's timeout seconds, this will continually count down their current remaining seconds every 5 seconds until there are no more seconds remaining. */ @@ -70,8 +70,8 @@ angular.module('umbraco.services') //if there are more than 30 remaining seconds, recurse! if (currentUser.remainingAuthSeconds > 30) { - //we need to check when the last time the timeout was set from the server, if - // it has been more than 30 seconds then we'll manually go and retrieve it from the + //we need to check when the last time the timeout was set from the server, if + // it has been more than 30 seconds then we'll manually go and retrieve it from the // server - this helps to keep our local countdown in check with the true timeout. if (lastServerTimeoutSet != null) { var now = new Date(); @@ -79,7 +79,7 @@ angular.module('umbraco.services') if (seconds > 30) { - //first we'll set the lastServerTimeoutSet to null - this is so we don't get back in to this loop while we + //first we'll set the lastServerTimeoutSet to null - this is so we don't get back in to this loop while we // wait for a response from the server otherwise we'll be making double/triple/etc... calls while we wait. lastServerTimeoutSet = null; @@ -98,7 +98,7 @@ angular.module('umbraco.services') } else { - //we are either timed out or very close to timing out so we need to show the login dialog. + //we are either timed out or very close to timing out so we need to show the login dialog. if (Umbraco.Sys.ServerVariables.umbracoSettings.keepUserLoggedIn !== true) { //NOTE: the safeApply because our timeout is set to not run digests (performance reasons) angularHelper.safeApply($rootScope, function () { @@ -109,14 +109,14 @@ angular.module('umbraco.services') } finally { userAuthExpired(); - } + } }); } else { //we've got less than 30 seconds remaining so let's check the server if (lastServerTimeoutSet != null) { - //first we'll set the lastServerTimeoutSet to null - this is so we don't get back in to this loop while we + //first we'll set the lastServerTimeoutSet to null - this is so we don't get back in to this loop while we // wait for a response from the server otherwise we'll be making double/triple/etc... calls while we wait. lastServerTimeoutSet = null; @@ -211,8 +211,8 @@ angular.module('umbraco.services') return result; }); }, - - /** Logs the user out + + /** Logs the user out */ logout: function () { diff --git a/src/Umbraco.Web.UI.Client/src/common/services/util.service.js b/src/Umbraco.Web.UI.Client/src/common/services/util.service.js index be5e2e1232..ba395becfc 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/util.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/util.service.js @@ -71,7 +71,7 @@ function dateHelper() { .minutes(Math.abs(serverOffsetMinutes)) .format('HH:mm'); - var server = moment.utc(momentLocal).zone(formattedOffset); + var server = moment.utc(momentLocal).utcOffset(formattedOffset); return server.format(format ? format : "YYYY-MM-DD HH:mm:ss"); }, diff --git a/src/Umbraco.Web.UI.Client/src/config/grid.editors.config.js b/src/Umbraco.Web.UI.Client/src/config/grid.editors.config.js index 3b1e2b7083..8301f15a14 100644 --- a/src/Umbraco.Web.UI.Client/src/config/grid.editors.config.js +++ b/src/Umbraco.Web.UI.Client/src/config/grid.editors.config.js @@ -39,7 +39,7 @@ "view": "textstring", "icon": "icon-quote", "config": { - "style": "border-left: 3px solid #ccc; padding: 10px; color: #ccc; font-family: serif; font-variant: italic; font-size: 18px", + "style": "border-left: 3px solid #ccc; padding: 10px; color: #ccc; font-family: serif; font-style: italic; font-size: 18px", "markup": "
#value#
" } } diff --git a/src/Umbraco.Web.UI.Client/src/controllers/search.controller.js b/src/Umbraco.Web.UI.Client/src/controllers/search.controller.js index 386e9a6712..54519f353b 100644 --- a/src/Umbraco.Web.UI.Client/src/controllers/search.controller.js +++ b/src/Umbraco.Web.UI.Client/src/controllers/search.controller.js @@ -100,7 +100,6 @@ function SearchController($scope, searchService, $log, $location, navigationServ //a canceler exists, so perform the cancelation operation and reset if (canceler) { - console.log("CANCELED!"); canceler.resolve(); canceler = $q.defer(); } diff --git a/src/Umbraco.Web.UI.Client/src/installer/installer.service.js b/src/Umbraco.Web.UI.Client/src/installer/installer.service.js index 4b732bf56e..5cb8e6230f 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/installer.service.js +++ b/src/Umbraco.Web.UI.Client/src/installer/installer.service.js @@ -17,7 +17,7 @@ angular.module("umbraco.install").factory('installerService', function($rootScop //add to umbraco installer facts here var facts = ['Umbraco helped millions of people watch a man jump from the edge of space', - 'Over 300 000 websites are currently powered by Umbraco', + 'Over 370 000 websites are currently powered by Umbraco', "At least 2 people have named their cat 'Umbraco'", 'On an average day, more than 1000 people download Umbraco', 'umbraco.tv is the premier source of Umbraco video tutorials to get you started', @@ -56,7 +56,7 @@ angular.module("umbraco.install").factory('installerService', function($rootScop return (found) ? found.description : null; } - //calculates the offset of the progressbar on the installaer + //calculates the offset of the progressbar on the installer function calculateProgress(steps, next) { var sorted = _.sortBy(steps, "serverOrder"); diff --git a/src/Umbraco.Web.UI.Client/src/installer/steps/starterkit.html b/src/Umbraco.Web.UI.Client/src/installer/steps/starterkit.html index 78e90445db..0b9e22a0f3 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/steps/starterkit.html +++ b/src/Umbraco.Web.UI.Client/src/installer/steps/starterkit.html @@ -12,7 +12,7 @@
  • Loading... - {{pck.name}} + {{pck.name}}
  • diff --git a/src/Umbraco.Web.UI.Client/src/less/application/grid.less b/src/Umbraco.Web.UI.Client/src/less/application/grid.less index 02f56e4046..9d1ec8cd32 100644 --- a/src/Umbraco.Web.UI.Client/src/less/application/grid.less +++ b/src/Umbraco.Web.UI.Client/src/less/application/grid.less @@ -118,6 +118,12 @@ body { } } +@media (max-width: 500px) { + #search-form .form-search { + width: ~"(calc(~'100%' - ~'80px'))"; + } +} + #navigation { left: 80px; top: 0; @@ -217,19 +223,17 @@ body { padding-left:20px } - - @media (max-width: 767px) { // make leftcolumn smaller on tablets #leftcolumn { - width: 60px; + width: 61px; } #applications ul.sections { - width: 60px; + width: 61px; } ul.sections.sections-tray { - width: 60px; + width: 61px; } #applications-tray { left: 60px; @@ -241,35 +245,24 @@ body { left: 30px; } #umbracoMainPageBody .umb-modal-left.fade.in { - margin-left: 60px; + margin-left: 61px; } } - - @media (max-width: 500px) { // make leftcolumn smaller on mobiles #leftcolumn { - width: 40px; + width: 41px; } #applications ul.sections { - width: 40px; - } - ul.sections li [class^="icon-"]:before { - font-size: 25px!important; - } - #applications ul.sections li.avatar a img { - width: 25px; - } - ul.sections a span { - display:none !important; + width: 41px; } #applications ul.sections-tray { - width: 40px; + width: 41px; } ul.sections.sections-tray { - width: 40px; + width: 41px; } #applications-tray { left: 40px; @@ -281,7 +274,7 @@ body { left: 20px; } #umbracoMainPageBody .umb-modal-left.fade.in { - margin-left: 40px; + margin-left: 41px; width: 85%!important; } } diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index 3a55e87382..2b53fbddfc 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -60,6 +60,9 @@ @import "application/shadows.less"; @import "application/animations.less"; +// Utilities +@import "utilities/_font-weight.less"; + // Belle styles @import "buttons.less"; @import "forms.less"; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor.less b/src/Umbraco.Web.UI.Client/src/less/components/editor.less index 6d43286f2d..00dd8592ff 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/editor.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/editor.less @@ -2,32 +2,42 @@ contains styling for all main editor directives */ -.umb-editor-wrapper{ +.umb-editor-wrapper { background: white; position: absolute; - top: 0px; bottom: 0px; left: 0px; right: 0px; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + flex-direction: column; + + > form { + display: flex; + flex-direction: column; + height: 100%; + } } - -.umb-editor-header{ +.umb-editor-header { background: @grayLighter; border-bottom: 1px solid @grayLight; - position: absolute; - height: 99px; - top: 0px; - right: 0px; - left: 0px; + flex: 0 0 99px; + position: relative; } +.umb-editor-header__actions-menu { + margin-left: auto; +} .umb-editor-container { - top: 101px; - left: 0px; - right: 0px; - bottom: 52px; - position: absolute; - clear: both; + position: relative; + top: 0; + right: 0; + bottom: 0; + left: 0; overflow: auto; + flex: 1 1 100%; } .umb-editor-wrapper.-no-footer .umb-editor-container { @@ -38,18 +48,12 @@ overflow: hidden; } -.umb-editor-drawer{ +.umb-editor-drawer { margin: 0; padding: 10px 20px; - z-index: 999; - position: absolute; - bottom: 0px; - left: 0px; - right: 0px; - height: 31px; - background: @grayLighter; border-top: 1px solid @grayLight; + flex: 1 0 31px; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less b/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less index 8c1acd97d8..0792925571 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less @@ -12,7 +12,7 @@ .umb-editor-sub-header.-umb-sticky-bar { box-shadow: 0 5px 0 rgba(0, 0, 0, 0.08), 0 1px 0 rgba(0, 0, 0, 0.16); transition: box-shadow 1s; - top: 101px; /* height of header: 100px + its bottom-border: 1px */ + top: 100px; /* height of header */ margin-top: 0; margin-bottom: 0; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/overlays.less b/src/Umbraco.Web.UI.Client/src/less/components/overlays.less index 0af0555b89..7a7ac15c6b 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/overlays.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/overlays.less @@ -133,7 +133,7 @@ bottom: 0; border: none; box-shadow: 0 0 20px rgba(0,0,0,0.19), 0 0 6px rgba(0,0,0,0.23); - margin-left: 80px; + margin-left: 81px; } .umb-overlay.umb-overlay-left .umb-overlay-header { @@ -148,7 +148,14 @@ @media (max-width: 767px) { .umb-overlay.umb-overlay-left { - margin-left: 60px; + margin-left: 61px; + } +} + +@media (max-width: 500px) { + .umb-overlay.umb-overlay-left { + margin-left: 41px; + width: ~"(calc(~'100%' - ~'41px'))"; } } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-breadcrumbs.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-breadcrumbs.less index 5854599722..d09946dfa3 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-breadcrumbs.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-breadcrumbs.less @@ -2,16 +2,22 @@ list-style: none; margin-bottom: 0; margin-left: 0; + display: flex; + flex-wrap: wrap; } .umb-breadcrumbs__ancestor { - display: inline-block; + display: flex; } .umb-breadcrumbs__ancestor-link, .umb-breadcrumbs__ancestor-text { - font-size: 11px; - color: #555; + font-size: 11px; + color: #555; + max-width: 150px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .umb-breadcrumbs__ancestor-link { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-lightbox.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-lightbox.less index e8477396bf..9f1ef219a9 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-lightbox.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-lightbox.less @@ -19,7 +19,7 @@ right: 0; bottom: 0; left: 0; - background: rgba(0, 0, 0, 0.2); + background: rgba(21, 21, 23, 0.7); width: 100%; height: 100%; } @@ -27,7 +27,14 @@ .umb-lightbox__close { position: absolute; top: 20px; - right: 20px; + right: 60px; +} + +.umb-lightbox__close i { + font-size: 20px; + cursor: pointer; + height: 40px; + width: 40px; } .umb-lightbox__images { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less index d3af5164f1..f14df918c0 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less @@ -9,6 +9,14 @@ padding: 20px 60px; } +@media (max-width: 768px) { + + .umb-packages-view-wrapper { + padding: 0; + } + +} + .umb-packages-section { margin-bottom: 40px; } @@ -323,6 +331,11 @@ width: 100%; } +.umb-era-button.umb-button--s { + height: 30px; + font-size: 13px; +} + /* CATEGORIES */ @@ -426,17 +439,35 @@ a.umb-package-details__back-link { } -@sidebarwidthFlax: 350px; // Width of sidebar. Ugly hack because of old version of Less +@sidebarwidth: 350px; // Width of sidebar. Ugly hack because of old version of Less .umb-package-details__main-content { flex: 1 1 auto; margin-right: 40px; - - width: ~"calc(100% - @{sidebarwidthFlax})"; // Make sure that the main content area doesn't gets affected by inline styling + width: ~"(calc(~'100%' - ~'@{sidebarwidth}' - ~'40px'))"; // Make sure that the main content area doesn't gets affected by inline styling } .umb-package-details__sidebar { - flex: 0 0 350px; + flex: 0 0 @sidebarwidth; +} + +@media (max-width: 768px) { + + .umb-package-details { + flex-direction: column; + } + + .umb-package-details__main-content { + flex: 1 1 auto; + width: 100%; + margin-bottom: 30px; + margin-right: 0; + } + + .umb-package-details__sidebar { + flex: 1 1 auto; + width: 100%; + } } .umb-package-details__section { @@ -515,6 +546,7 @@ a.umb-package-details__back-link { } .umb-package-details__owner-profile-avatar { margin-right: 15px; + flex: 0 0 auto; } .umb-package-details__owner-profile-name { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less index 874f9e9551..bc8e19cf2b 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less @@ -3,6 +3,8 @@ display: flex; flex-direction: column; + position: relative; + border: 1px solid @grayLight; flex-wrap: nowrap; @@ -11,6 +13,25 @@ min-width: 640px; } +.umb-table.umb-table-inactive { + + &:before { + content: ""; + background: rgba(255, 255, 255, 0.75); + + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + + z-index: 10; + + outline: 1px solid rgba(255, 255, 255, 0.75); + } + +} + .umb-table a { text-decoration: none; cursor: pointer; @@ -93,6 +114,14 @@ input.umb-table__input { } } +.umb-table-body .umb-table-row.-solid { + cursor: default; + + &:hover { + background-color: white; + } +} + .umb-table-body__link { text-decoration: none; @@ -183,6 +212,8 @@ input.umb-table__input { user-select: none; } + + .umb-table-row.-selected, .umb-table-row.-selected:hover { background-color: fade(@blueDark, 4%); @@ -212,7 +243,7 @@ input.umb-table__input { text-overflow: ellipsis; } -.umb-table-cell:first-of-type { +.umb-table-cell:first-of-type:not(.not-fixed) { flex: 0 0 25px; margin: 0 0 0 15px; diff --git a/src/Umbraco.Web.UI.Client/src/less/healthcheck.less b/src/Umbraco.Web.UI.Client/src/less/healthcheck.less index a5f2d454f1..6e82d424bb 100644 --- a/src/Umbraco.Web.UI.Client/src/less/healthcheck.less +++ b/src/Umbraco.Web.UI.Client/src/less/healthcheck.less @@ -1,12 +1,20 @@ -/* Vars */ - @red-orange: #FF3F34; - @sunrise: #F5D226; - @emerald: #50C878; - .umb-healthcheck { display: flex; flex-wrap: wrap; + margin-left: -10px; + margin-right: -10px; +} + +.umb-healthcheck-help-text { + line-height: 1.6em; + margin-bottom: 30px; +} + +.umb-healthcheck-action-bar { + display: flex; + justify-content: flex-end; + margin-bottom: 20px; } @@ -42,7 +50,6 @@ /* Title */ .umb-healthcheck-title { - margin-bottom: 15px; font-size: 14px; font-weight: bold; } @@ -50,28 +57,26 @@ /* Messages */ .umb-healthcheck-messages { - display: flex; - align-items: center; - justify-content: center; - flex-direction: column; + margin-top: 15px; } .umb-healthcheck-message { position: relative; background: #fff; border-radius: 50px; - display: flex; - align-items: center; - flex: 1 1 auto; - padding: 5px 10px; + display: inline-flex; + align-items: center; + padding-left: 8px; + padding-right: 8px; margin-bottom: 5px; color: #000; font-weight: bold; + font-size: 13px; } .umb-healthcheck-message i { - font-size: 20px; - margin-right: 5px; + font-size: 15px; + margin-right: 3px; } .umb-healthcheck-details-link { @@ -99,11 +104,11 @@ font-size: 14px; font-weight: bold; - height: 38px; + height: 40px; line-height: 1; max-width: 100%; - padding: 0 18px; + padding: 0 15px; color: #484848; background-color: #e0e0e0; @@ -146,6 +151,15 @@ background-color: @blueDark; } +.umb-era-button.-red { + background: @btnDangerBackground; + color: white; +} + +.umb-era-button.-red:hover { + background-color: darken(@btnDangerBackground, 5%); +} + .umb-era-button.-link { padding: 0; background: transparent; @@ -235,12 +249,13 @@ } .umb-healthcheck-group__details-checks { - border: 2px solid @grayLight; + border: 1px solid @grayLight; border-top: none; border-radius: 0 0 3px 3px; } .umb-healthcheck-group__details-check { + position: relative; } .umb-healthcheck-group__details-check-title { @@ -252,20 +267,19 @@ font-size: 14px; color: @black; font-weight: bold; + margin-bottom: 2px; } .umb-healthcheck-group__details-check-description { font-size: 12px; color: @grayMed; - line-height: 1.6rem; - //margin-top: 10px; + line-height: 1.6em; } .umb-healthcheck-group__details-status { padding: 15px 0; display: flex; border-bottom: 2px solid @grayLighter; - position: relative; } .umb-healthcheck-group__details-status-overlay { @@ -328,6 +342,6 @@ } .umb-healthcheck-group__details-status-action-description { - margin-top: 10px; - font-size: 13px; + margin-top: 5px; + font-size: 11px; } diff --git a/src/Umbraco.Web.UI.Client/src/less/listview.less b/src/Umbraco.Web.UI.Client/src/less/listview.less index 88762edf0c..95c8a49b36 100644 --- a/src/Umbraco.Web.UI.Client/src/less/listview.less +++ b/src/Umbraco.Web.UI.Client/src/less/listview.less @@ -166,51 +166,6 @@ background: none } -.umb-listview .pagination { - margin: 0; -} - -.umb-listview .table th { - font-weight: normal -} - -.umb-listview .showing { - padding: 8px 4px 2px 4px; - background: none; - font-size: 11px; - color: #b0b0b0 -} - -.umb-listview .pagination { - text-align: center; -} - -.umb-listview .pagination ul { - -webkit-border-radius: 0px; - -moz-border-radius: 0px; - border-radius: 0px; - -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0); - -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0); - box-shadow: 0 1px 2px rgba(0, 0, 0, 0); - background: none -} - -.umb-listview .pagination ul > li > a, .pagination ul > li > span { - border:none; - padding: 8px 4px 2px 4px; - background: none; - font-size: 12px; - color: #b0b0b0 -} - -.umb-listview .pagination ul > li.active > a, .umb-listview .pagination ul > li > a:hover { - color: @black; -} - -.umb-listview .pagination ul > li.disabled > a, .umb-listview .pagination ul > li.disabled > a:hover { - color: @grayLight; -} - /* TEMP */ .umb-listview .table-striped tbody td { diff --git a/src/Umbraco.Web.UI.Client/src/less/modals.less b/src/Umbraco.Web.UI.Client/src/less/modals.less index 0c2c2f07ad..8ac5af175c 100644 --- a/src/Umbraco.Web.UI.Client/src/less/modals.less +++ b/src/Umbraco.Web.UI.Client/src/less/modals.less @@ -114,7 +114,7 @@ .umbracoDialog{ width: auto !Important; height: auto !Important; - padding: 20px; + padding: 20px; } .umbracoDialog .umb-pane{margin-left: 0px; margin-right: 0px; margin-top: 0px;} .umbracoDialog .umb-dialog-body .umb-pane{margin-left: 20px; margin-right: 20px; margin-top: 20px;} @@ -125,7 +125,11 @@ .umb-modal .controls-row{margin-left: 0px !important;} /* modal and umb-modal are used for right.hand dialogs */ -.modal.fade.in{border: none !important; border-radius: none !important;} +.modal { + border-radius: 0 !important; + + &.fade.in{border: none !important;} +} .umb-modal.fade { outline: none; top: 0 !important; diff --git a/src/Umbraco.Web.UI.Client/src/less/panel.less b/src/Umbraco.Web.UI.Client/src/less/panel.less index aab0c5987c..c80f72921c 100644 --- a/src/Umbraco.Web.UI.Client/src/less/panel.less +++ b/src/Umbraco.Web.UI.Client/src/less/panel.less @@ -483,13 +483,15 @@ input.umb-panel-header-description { .umb-editor-drawer-content { display: flex; align-items: center; - //justify-content: space-between; } .umb-editor-drawer-content__right-side { margin-left: auto; + flex: 0 0 auto; + padding-left: 10px; } .umb-editor-drawer-content__left-side { margin-right: auto; + padding-right: 10px; } diff --git a/src/Umbraco.Web.UI.Client/src/less/property-editors.less b/src/Umbraco.Web.UI.Client/src/less/property-editors.less index 85a1d9e36c..16d48807eb 100644 --- a/src/Umbraco.Web.UI.Client/src/less/property-editors.less +++ b/src/Umbraco.Web.UI.Client/src/less/property-editors.less @@ -704,6 +704,31 @@ ul.color-picker li a { padding-top: 27px; } +.umb-fileupload .file-icon { + text-align: center; + display: block; + position: relative; + padding: 5px 0; + + > .icon { + font-size: 70px; + line-height: 110%; + color: #666; + text-align: center; + } + + > span { + color: #fff; + background: #666; + padding: 1px 3px; + font-size: 12px; + line-height: 130%; + position: absolute; + top: 45px; + left: 110px; + } +} + .umb-fileupload input { font-size: 12px; line-height: 1; diff --git a/src/Umbraco.Web.UI.Client/src/less/sections.less b/src/Umbraco.Web.UI.Client/src/less/sections.less index c89be83a10..c764280c8d 100644 --- a/src/Umbraco.Web.UI.Client/src/less/sections.less +++ b/src/Umbraco.Web.UI.Client/src/less/sections.less @@ -18,8 +18,8 @@ ul.sections li { transition: all .3s linear; } -ul.sections li [class^="icon-"]:before, -ul.sections li [class*=" icon-"]:before, +ul.sections li [class^="icon-"], +ul.sections li [class*=" icon-"], ul.sections li img.icon-section { font-size: 30px; line-height: 20px; /* set line-height to ensure all icons use same line-height */ @@ -31,8 +31,8 @@ ul.sections li img.icon-section { transition: all .3s linear; } -ul.sections:hover li [class^="icon-"]:before, -ul.sections:hover li [class*=" icon-"]:before, +ul.sections:hover li [class^="icon-"], +ul.sections:hover li [class*=" icon-"], ul.sections:hover li img.icon-section { opacity: 1 } @@ -111,32 +111,43 @@ ul.sections li.help { bottom: 0; left: 0; display: block; - width: 100%; + width: ~"(calc(~'100%' - ~'5px'))"; //subtract 4px orange border + 1px border-right for sections } ul.sections li.help a { border-bottom: none; } +@media (max-width: 500px) { + ul.sections li [class^="icon-"], + ul.sections li [class*=" icon-"] { + font-size: 25px; + } + ul.sections li:not(.avatar) a { + padding-top: 12px; + padding-bottom: 6px; + + .icon, .icon-section { + display: inline-block; + padding-left: 2px; + } + } + ul.sections a span { + display:none; + } +} + // Section slide-out tray for additional apps // ------------------------- -li.expand a, li.expand{border: none !Important;} +li.expand a, li.expand{border: none !important;} li.expand { + > a > i.icon { + transition: all .3s linear; + } &.open > a > i.icon { - -webkit-transition: all 1s !important; - -o-transition: all 1s !important; - -moz-transition: all 1s !important; - transition: all 1s !important; - - &:before { - -ms-transform: rotate(180deg) !important; /* IE 9 */ - -webkit-transform: rotate(180deg) !important; /* Chrome, Safari, Opera */ - -o-transform: rotate(180deg) !important; - -moz-transform: rotate(180deg) !important; - transform: rotate(180deg) !important; - } + transform: rotate(180deg); } } diff --git a/src/Umbraco.Web.UI.Client/src/less/utilities/_flexbox.less b/src/Umbraco.Web.UI.Client/src/less/utilities/_flexbox.less index 829aba08f7..c0815fa8ac 100644 --- a/src/Umbraco.Web.UI.Client/src/less/utilities/_flexbox.less +++ b/src/Umbraco.Web.UI.Client/src/less/utilities/_flexbox.less @@ -1,37 +1,96 @@ /* + Flexbox + */ +.flex { display: flex; } +.flex-inline { display: inline-flex; } -.flex { display: flex } +.flex-column { flex-direction: column; } +.flex-wrap { flex-wrap: wrap; } -.flex-column { flex-direction: column } -.flex-wrap { flex-wrap: wrap } +.items-start { align-items: flex-start; } +.items-end { align-items: flex-end; } +.items-center { align-items: center; } +.items-baseline { align-items: baseline; } +.items-stretch { align-items: stretch; } -.items-start { align-items: flex-start } -.items-end { align-items: flex-end } -.items-center { align-items: center } -.items-baseline { align-items: baseline } -.items-stretch { align-items: stretch } +.self-start { align-self: flex-start; } +.self-end { align-self: flex-end; } +.self-center { align-self: center; } +.self-baseline { align-self: baseline; } +.self-stretch { align-self: stretch; } -.self-start { align-self: flex-start } -.self-end { align-self: flex-end } -.self-center { align-self: center } -.self-baseline { align-self: baseline } -.self-stretch { align-self: stretch } +.justify-start { justify-content: flex-start; } +.justify-end { justify-content: flex-end; } +.justify-center { justify-content: center; } +.justify-between { justify-content: space-between; } +.justify-around { justify-content: space-around; } -.justify-start { justify-content: flex-start } -.justify-end { justify-content: flex-end } -.justify-center { justify-content: center } -.justify-between { justify-content: space-between } -.justify-around { justify-content: space-around } +.content-start { align-content: flex-start; } +.content-end { align-content: flex-end; } +.content-center { align-content: center; } +.content-between { align-content: space-between; } +.content-around { align-content: space-around; } +.content-stretch { align-content: stretch; } -.content-start { align-content: flex-start } -.content-end { align-content: flex-end } -.content-center { align-content: center } -.content-between { align-content: space-between } -.content-around { align-content: space-around } -.content-stretch { align-content: stretch } +.flx-i { + flex: 1; +} + + +.flx-g0 { + flex-grow: 0; +} +.flx-g1 { + flex-grow: 1; +} + +.flx-s0 { + flex-shrink: 0; +} +.flx-s1 { + flex-shrink: 1; +} + + +.flx-b0 { + flex-basis: 0%; +} +.flx-b1 { + flex-basis: 10%; +} +.flx-b2 { + flex-basis: 20%; +} +.flx-b3 { + flex-basis: 30%; +} +.flx-b4 { + flex-basis: 40%; +} +.flx-b5 { + flex-basis: 50%; +} +.flx-b6 { + flex-basis: 60%; +} +.flx-b7 { + flex-basis: 70%; +} +.flx-b8 { + flex-basis: 80%; +} +.flx-b9 { + flex-basis: 90%; +} +.flx-b10 { + flex-basis: 100%; +} +.flx-ba { + flex-basis: auto; +} /* 1. Fix for Chrome 44 bug. https://code.google.com/p/chromium/issues/detail?id=506893 */ .flex-auto { @@ -40,4 +99,4 @@ min-height: 0; /* 1 */ } -.flex-none { flex: none } +.flex-none { flex: none; } diff --git a/src/Umbraco.Web.UI.Client/src/less/utilities/_font-weight.less b/src/Umbraco.Web.UI.Client/src/less/utilities/_font-weight.less new file mode 100644 index 0000000000..111ed4b2ef --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/utilities/_font-weight.less @@ -0,0 +1,10 @@ +/* + FONT WEIGHT +*/ + + + +.light { font-weight: 300; } +.normal { font-weight: 500; } +.semi-bold { font-weight: 600; } +.bold { font-weight: 700; } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js index c38c55fcfb..aa037b7431 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js @@ -79,7 +79,7 @@ } $scope.loginSubmit = function (login, password) { - + //if the login and password are not empty we need to automatically // validate them - this is because if there are validation errors on the server // then the user has to change both username & password to resubmit which isn't ideal, @@ -121,13 +121,18 @@ $scope.requestPasswordResetSubmit = function (email) { - $scope.errorMsg = ""; + if (email && email.length > 0) { + $scope.requestPasswordResetForm.email.$setValidity('auth', true); + } + $scope.showEmailResetConfirmation = false; if ($scope.requestPasswordResetForm.$invalid) { return; } + $scope.errorMsg = ""; + authResource.performRequestPasswordReset(email) .then(function () { //remove the email entered diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html index 12f25f42a5..5933848c36 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html @@ -25,7 +25,7 @@ id="{{login.authType}}" name="provider" value="{{login.authType}}" title="Log in using your {{login.caption}} account"> - Sign in with {{login.caption}} + Sign in with {{login.caption}} @@ -33,7 +33,7 @@

    -
    Or
    +
    or
    diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/rteembed.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/rteembed.controller.js index f2052ccc65..0c1cb5b62e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/rteembed.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/rteembed.controller.js @@ -32,7 +32,7 @@ break; case 1: //error - $scope.form.info = "Computer says no"; + $scope.form.info = "Could not embed media - please ensure the URL is valid"; break; case 2: $scope.form.preview = data.Markup; @@ -44,7 +44,7 @@ .error(function () { $scope.form.supportsDimensions = false; $scope.form.preview = ""; - $scope.form.info = "Computer says no"; + $scope.form.info = "Could not embed media - please ensure the URL is valid"; }); } else { $scope.form.supportsDimensions = false; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.html b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.html index 83be1a7782..2b2d3bd1cf 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.html @@ -46,18 +46,18 @@
    -
    + + + -
    + + Link your {{login.caption}} account + + - + + Link your {{login.caption}} account + + - + +
    @@ -167,7 +167,7 @@
    diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/examinemgmt.controller.js b/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/examinemgmt.controller.js index 4a19ea9926..5f6bb23001 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/examinemgmt.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/examinemgmt.controller.js @@ -1,4 +1,4 @@ -function examineMgmtController($scope, umbRequestHelper, $log, $http, $q, $timeout) { +function ExamineMgmtController($scope, umbRequestHelper, $log, $http, $q, $timeout) { $scope.indexerDetails = []; $scope.searcherDetails = []; @@ -6,7 +6,9 @@ function examineMgmtController($scope, umbRequestHelper, $log, $http, $q, $timeo function checkProcessing(indexer, checkActionName) { umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("examineMgmtBaseUrl", checkActionName, { indexerName: indexer.name })), + $http.post(umbRequestHelper.getApiUrl("examineMgmtBaseUrl", + checkActionName, + { indexerName: indexer.name })), 'Failed to check index processing') .then(function(data) { @@ -17,70 +19,73 @@ function examineMgmtController($scope, umbRequestHelper, $log, $http, $q, $timeo indexer[k] = data[k]; } indexer.isProcessing = false; - } - else { - $timeout(function () { - //don't continue if we've tried 100 times - if (indexer.processingAttempts < 100) { - checkProcessing(indexer, checkActionName); - //add an attempt - indexer.processingAttempts++; - } - else { - //we've exceeded 100 attempts, stop processing - indexer.isProcessing = false; - } - }, 1000); + } else { + $timeout(function() { + //don't continue if we've tried 100 times + if (indexer.processingAttempts < 100) { + checkProcessing(indexer, checkActionName); + //add an attempt + indexer.processingAttempts++; + } else { + //we've exceeded 100 attempts, stop processing + indexer.isProcessing = false; + } + }, + 1000); } }); } - $scope.search = function (searcher, e) { + $scope.search = function(searcher, e) { if (e && e.keyCode !== 13) { return; } umbRequestHelper.resourcePromise( - $http.get(umbRequestHelper.getApiUrl("examineMgmtBaseUrl", "GetSearchResults", { - searcherName: searcher.name, - query: encodeURIComponent(searcher.searchText), - queryType: searcher.searchType - })), + $http.get(umbRequestHelper.getApiUrl("examineMgmtBaseUrl", + "GetSearchResults", + { + searcherName: searcher.name, + query: encodeURIComponent(searcher.searchText), + queryType: searcher.searchType + })), 'Failed to search') .then(function(searchResults) { searcher.isSearching = true; searcher.searchResults = searchResults; }); } - + $scope.toggle = function(provider, propName) { if (provider[propName] !== undefined) { provider[propName] = !provider[propName]; - } - else { + } else { provider[propName] = true; } } $scope.rebuildIndex = function(indexer) { if (confirm("This will cause the index to be rebuilt. " + - "Depending on how much content there is in your site this could take a while. " + - "It is not recommended to rebuild an index during times of high website traffic " + - "or when editors are editing content.")) { + "Depending on how much content there is in your site this could take a while. " + + "It is not recommended to rebuild an index during times of high website traffic " + + "or when editors are editing content.")) { indexer.isProcessing = true; indexer.processingAttempts = 0; umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("examineMgmtBaseUrl", "PostRebuildIndex", { indexerName: indexer.name })), + $http.post(umbRequestHelper.getApiUrl("examineMgmtBaseUrl", + "PostRebuildIndex", + { indexerName: indexer.name })), 'Failed to rebuild index') - .then(function () { + .then(function() { //rebuilding has started, nothing is returned accept a 200 status code. //lets poll to see if it is done. - $timeout(function () { - checkProcessing(indexer, "PostCheckRebuildIndex"); - }, 1000); + $timeout(function() { + checkProcessing(indexer, "PostCheckRebuildIndex"); + }, + 1000); }); } @@ -88,20 +93,23 @@ function examineMgmtController($scope, umbRequestHelper, $log, $http, $q, $timeo $scope.optimizeIndex = function(indexer) { if (confirm("This will cause the index to be optimized which will improve its performance. " + - "It is not recommended to optimize an index during times of high website traffic " + - "or when editors are editing content.")) { + "It is not recommended to optimize an index during times of high website traffic " + + "or when editors are editing content.")) { indexer.isProcessing = true; umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("examineMgmtBaseUrl", "PostOptimizeIndex", { indexerName: indexer.name })), + $http.post(umbRequestHelper.getApiUrl("examineMgmtBaseUrl", + "PostOptimizeIndex", + { indexerName: indexer.name })), 'Failed to optimize index') - .then(function () { + .then(function() { //optimizing has started, nothing is returned accept a 200 status code. //lets poll to see if it is done. - $timeout(function () { - checkProcessing(indexer, "PostCheckOptimizeIndex"); - }, 1000); + $timeout(function() { + checkProcessing(indexer, "PostCheckOptimizeIndex"); + }, + 1000); }); } @@ -111,36 +119,34 @@ function examineMgmtController($scope, umbRequestHelper, $log, $http, $q, $timeo searcher.isSearching = true; } - //go get the data //combine two promises and execute when they are both done $q.all([ - //get the indexer details - umbRequestHelper.resourcePromise( - $http.get(umbRequestHelper.getApiUrl("examineMgmtBaseUrl", "GetIndexerDetails")), - 'Failed to retrieve indexer details') - .then(function(data) { - $scope.indexerDetails = data; - }), - - //get the searcher details - umbRequestHelper.resourcePromise( - $http.get(umbRequestHelper.getApiUrl("examineMgmtBaseUrl", "GetSearcherDetails")), - 'Failed to retrieve searcher details') - .then(function(data) { - $scope.searcherDetails = data; - for (var s in $scope.searcherDetails) { - $scope.searcherDetails[s].searchType = "text"; - } - }) - - ]).then(function () { - //all init loading is complete - $scope.loading = false; - }); - + //get the indexer details + umbRequestHelper.resourcePromise( + $http.get(umbRequestHelper.getApiUrl("examineMgmtBaseUrl", "GetIndexerDetails")), + 'Failed to retrieve indexer details') + .then(function(data) { + $scope.indexerDetails = data; + }), + //get the searcher details + umbRequestHelper.resourcePromise( + $http.get(umbRequestHelper.getApiUrl("examineMgmtBaseUrl", "GetSearcherDetails")), + 'Failed to retrieve searcher details') + .then(function(data) { + $scope.searcherDetails = data; + for (var s in $scope.searcherDetails) { + $scope.searcherDetails[s].searchType = "text"; + } + }) + ]) + .then(function() { + //all init loading is complete + $scope.loading = false; + }); } -angular.module("umbraco").controller("Umbraco.Dashboard.ExamineMgmtController", examineMgmtController); \ No newline at end of file + +angular.module("umbraco").controller("Umbraco.Dashboard.ExamineMgmtController", ExamineMgmtController); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/healthcheck.controller.js b/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/healthcheck.controller.js index d9c0d0774b..8631b09a45 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/healthcheck.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/healthcheck.controller.js @@ -1,8 +1,7 @@ -(function () { +(function() { "use strict"; function HealthCheckController($scope, healthCheckResource) { - var SUCCESS = 0; var WARNING = 1; var ERROR = 2; @@ -11,73 +10,49 @@ var vm = this; vm.viewState = "list"; - vm.groups = []; + vm.groups = []; vm.selectedGroup = {}; - vm.getStatus = getStatus; - vm.executeAction = executeAction; - vm.checkAllInGroup = checkAllInGroup; + vm.getStatus = getStatus; + vm.executeAction = executeAction; + vm.checkAllGroups = checkAllGroups; + vm.checkAllInGroup = checkAllInGroup; vm.openGroup = openGroup; vm.setViewState = setViewState; - vm.triggerChecks = triggerChecks; - vm.checksRunning = false; - vm.totalGroups = 0; - vm.totalGroupsChecked = 0; - - function triggerChecks(){ - - //Reset counter - inczse - - //Checks running - hide button that triggers check - //So we can't invoke multiple times & make further blocking requests - vm.checksRunning = true; - - // Get a (grouped) list of all health checks - healthCheckResource.getAllChecks().then( - function(response) { - - //Total number of groups - vm.totalGroups = response.length; - - // set number of checks which has been executed - for (var i = 0; i < response.length; i++) { - var group = response[i]; - group.checkCounter = 0; - checkAllInGroup(group, group.checks); - } - - vm.groups = response; - } - ); - } + // Get a (grouped) list of all health checks + healthCheckResource.getAllChecks() + .then(function(response) { + vm.groups = response; + }); function setGroupGlobalResultType(group) { - var totalSuccess = 0; var totalError = 0; var totalWarning = 0; var totalInfo = 0; // count total number of statusses - angular.forEach(group.checks, function(check){ - angular.forEach(check.status, function(status){ - switch(status.resultType) { - case SUCCESS: - totalSuccess = totalSuccess + 1; - break; - case WARNING: - totalWarning = totalWarning + 1; - break; - case ERROR: - totalError = totalError + 1; - break; - case INFO: - totalInfo = totalInfo + 1; - break; - } + angular.forEach(group.checks, + function(check) { + angular.forEach(check.status, + function(status) { + switch (status.resultType) { + case SUCCESS: + totalSuccess = totalSuccess + 1; + break; + case WARNING: + totalWarning = totalWarning + 1; + break; + case ERROR: + totalError = totalError + 1; + break; + case INFO: + totalInfo = totalInfo + 1; + break; + } + }); }); - }); group.totalSuccess = totalSuccess; group.totalError = totalError; @@ -86,55 +61,58 @@ } - // Get the status of an individual check - function getStatus(check) { - check.loading = true; - check.status = null; - healthCheckResource.getStatus(check.id).then(function(response) { - check.loading = false; - check.status = response; - }); - } + // Get the status of an individual check + function getStatus(check) { + check.loading = true; + check.status = null; + healthCheckResource.getStatus(check.id) + .then(function(response) { + check.loading = false; + check.status = response; + }); + } - function executeAction(check, index, action) { - healthCheckResource.executeAction(action).then(function (response) { - check.status[index] = response; - }); - } + function executeAction(check, index, action) { + check.loading = true; + healthCheckResource.executeAction(action) + .then(function(response) { + check.status[index] = response; + check.loading = false; + }); + } - function checkAllInGroup(group, checks) { + function checkAllGroups(groups) { + // set number of checks which has been executed + for (var i = 0; i < groups.length; i++) { + var group = groups[i]; + checkAllInGroup(group, group.checks); + } + vm.groups = groups; + } + function checkAllInGroup(group, checks) { group.checkCounter = 0; group.loading = true; - angular.forEach(checks, function(check) { + angular.forEach(checks, + function(check) { - check.loading = true; + check.loading = true; - healthCheckResource.getStatus(check.id).then(function(response) { - check.status = response; - group.checkCounter = group.checkCounter + 1; - check.loading = false; + healthCheckResource.getStatus(check.id) + .then(function(response) { + check.status = response; + group.checkCounter = group.checkCounter + 1; + check.loading = false; - // when all checks are done, set global group result - if (group.checkCounter === checks.length) { - setGroupGlobalResultType(group); - group.loading = false; - - //This group of checks run - increment counter by one - vm.totalGroupsChecked++; - - //Once we have all done all checks for this group - //Verify if this was last group or not with counters & reset button - if(vm.totalGroups === vm.totalGroupsChecked){ - vm.checksRunning = false; - } - } - - }); - }); - - } + // when all checks are done, set global group result + if (group.checkCounter === checks.length) { + setGroupGlobalResultType(group); + group.loading = false; + } + }); + }); + } function openGroup(group) { vm.selectedGroup = group; @@ -144,17 +122,14 @@ function setViewState(state) { vm.viewState = state; - if(state === 'list') { + if (state === 'list') { for (var i = 0; i < vm.groups.length; i++) { var group = vm.groups[i]; setGroupGlobalResultType(group); } - } - } - } angular.module("umbraco").controller("Umbraco.Dashboard.HealthCheckController", HealthCheckController); diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/healthcheck.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/healthcheck.html index 1135d14c73..ca796043e6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/healthcheck.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/healthcheck.html @@ -1,45 +1,55 @@
    -

    Health Check

    -

    The health checker evaluates various areas of your site for best practice settings, configuration, potential problems, etc. You can easily fix problems by pressing a button.
    - You can add your own health checks, have a look at the documentation for more information about custom health checks.

    - +
    -
    +

    Health Check

    +
    +

    The health checker evaluates various areas of your site for best practice settings, configuration, potential problems, etc. You can easily fix problems by pressing a button. + You can add your own health checks, have a look at the documentation for more information about custom health checks.

    +
    -
    -
    -
    {{group.name}}
    +
    + +
    -
    - -
    +
    -
    +
    +
    -
    - - {{ group.totalSuccess }} +
    {{group.name}}
    + +
    +
    -
    - - {{ group.totalWarning }} -
    +
    -
    - - {{ group.totalError }} -
    +
    + + {{ group.totalSuccess }} +
    + +
    + + {{ group.totalWarning }} +
    + +
    + + {{ group.totalError }} +
    + +
    + + {{ group.totalInfo }} +
    -
    - - {{ group.totalInfo }}
    -
    +
    @@ -48,7 +58,7 @@ - ← Take me back + ← Back to overview @@ -57,7 +67,7 @@
    {{ vm.selectedGroup.name }}
    - +
    @@ -72,10 +82,10 @@
    - - - - + + + +
    @@ -85,24 +95,37 @@
    -
    +
    - - Set new value: - - - + + +
    +
    + +
    + + + +
    +
    -
    -
    - -
    +
    +
    +
    +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/redirecturls.controller.js b/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/redirecturls.controller.js new file mode 100644 index 0000000000..7acab72455 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/redirecturls.controller.js @@ -0,0 +1,162 @@ +(function() { + "use strict"; + + function RedirectUrlsController($scope, redirectUrlsResource, notificationsService, localizationService, $q) { + //...todo + //search by url or url part + //search by domain + //display domain in dashboard results? + + //used to cancel any request in progress if another one needs to take it's place + var vm = this; + var canceler = null; + + vm.dashboard = { + searchTerm: "", + loading: false, + urlTrackerDisabled: false, + userIsAdmin: false + }; + + vm.pagination = { + pageIndex: 0, + pageNumber: 1, + totalPages: 1, + pageSize: 20 + }; + + vm.goToPage = goToPage; + vm.search = search; + vm.removeRedirect = removeRedirect; + vm.disableUrlTracker = disableUrlTracker; + vm.enableUrlTracker = enableUrlTracker; + vm.filter = filter; + vm.checkEnabled = checkEnabled; + + function activate() { + vm.checkEnabled().then(function() { + vm.search(); + }); + } + + function checkEnabled() { + vm.dashboard.loading = true; + return redirectUrlsResource.getEnableState().then(function (response) { + vm.dashboard.urlTrackerDisabled = response.enabled !== true; + vm.dashboard.userIsAdmin = response.userIsAdmin; + vm.dashboard.loading = false; + }); + } + + function goToPage(pageNumber) { + vm.pagination.pageIndex = pageNumber - 1; + vm.pagination.pageNumber = pageNumber; + vm.search(); + } + + function search() { + + vm.dashboard.loading = true; + + var searchTerm = vm.dashboard.searchTerm; + if (searchTerm === undefined) { + searchTerm = ""; + } + + redirectUrlsResource.searchRedirectUrls(searchTerm, vm.pagination.pageIndex, vm.pagination.pageSize).then(function(response) { + + vm.redirectUrls = response.searchResults; + + // update pagination + vm.pagination.pageIndex = response.currentPage; + vm.pagination.pageNumber = response.currentPage + 1; + vm.pagination.totalPages = response.pageCount; + + vm.dashboard.loading = false; + + }); + } + + function removeRedirect(redirectToDelete) { + localizationService.localize("redirectUrls_confirmRemove", [redirectToDelete.originalUrl, redirectToDelete.destinationUrl]).then(function (value) { + var toggleConfirm = confirm(value); + + if (toggleConfirm) { + redirectUrlsResource.deleteRedirectUrl(redirectToDelete.redirectId).then(function () { + + var index = vm.redirectUrls.indexOf(redirectToDelete); + vm.redirectUrls.splice(index, 1); + notificationsService.success(localizationService.localize("redirectUrls_redirectRemoved")); + + // check if new redirects needs to be loaded + if (vm.redirectUrls.length === 0 && vm.pagination.totalPages > 1) { + + // if we are not on the first page - get records from the previous + if (vm.pagination.pageIndex > 0) { + vm.pagination.pageIndex = vm.pagination.pageIndex - 1; + vm.pagination.pageNumber = vm.pagination.pageNumber - 1; + } + + search(); + } + }, function (error) { + notificationsService.error(localizationService.localize("redirectUrls_redirectRemoveError")); + }); + } + }); + } + + function disableUrlTracker() { + localizationService.localize("redirectUrls_confirmDisable").then(function(value) { + var toggleConfirm = confirm(value); + if (toggleConfirm) { + + redirectUrlsResource.toggleUrlTracker(true).then(function () { + activate(); + notificationsService.success(localizationService.localize("redirectUrls_disabledConfirm")); + }, function (error) { + notificationsService.warning(localizationService.localize("redirectUrls_disableError")); + }); + + } + }); + } + + function enableUrlTracker() { + redirectUrlsResource.toggleUrlTracker(false).then(function() { + activate(); + notificationsService.success(localizationService.localize("redirectUrls_enabledConfirm")); + }, function(error) { + notificationsService.warning(localizationService.localize("redirectUrls_enableError")); + }); + } + + var filterDebounced = _.debounce(function(e) { + + $scope.$apply(function() { + + //a canceler exists, so perform the cancelation operation and reset + if (canceler) { + canceler.resolve(); + canceler = $q.defer(); + } else { + canceler = $q.defer(); + } + + vm.search(); + + }); + + }, 200); + + function filter() { + vm.dashboard.loading = true; + filterDebounced(); + } + + activate(); + + } + + angular.module("umbraco").controller("Umbraco.Dashboard.RedirectUrlsController", RedirectUrlsController); +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/redirecturls.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/redirecturls.html new file mode 100644 index 0000000000..f23b8f5df9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/redirecturls.html @@ -0,0 +1,113 @@ +
    + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    +
    +
    Original URL
    +
    +
    Redirected To
    +
    +
    + +
    + +
    + + + + +
    + +
    + +
    + + + +
    + +
    + +
    + +
    + + +
    No redirects have been made
    + When a published page gets renamed or moved a redirect will automatically be made to the new page +
    + + + + + +
    + + +
    + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/xmldataintegrityreport.controller.js b/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/xmldataintegrityreport.controller.js index 09b638b05f..bb8696aca0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/xmldataintegrityreport.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/xmldataintegrityreport.controller.js @@ -1,4 +1,4 @@ -function xmlDataIntegrityReportController($scope, umbRequestHelper, $log, $http, $q, $timeout) { +function XmlDataIntegrityReportController($scope, umbRequestHelper, $log, $http) { function check(item) { var action = item.check; @@ -20,12 +20,12 @@ function xmlDataIntegrityReportController($scope, umbRequestHelper, $log, $http, "or when editors are editing content.")) { item.fixing = true; umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("xmlDataIntegrityBaseUrl", action)), - 'Failed to retrieve data integrity status') - .then(function (result) { - item.fixing = false; - item.invalid = result === "false"; - }); + $http.post(umbRequestHelper.getApiUrl("xmlDataIntegrityBaseUrl", action)), + 'Failed to retrieve data integrity status') + .then(function(result) { + item.fixing = false; + item.invalid = result === "false"; + }); } } } @@ -57,6 +57,6 @@ function xmlDataIntegrityReportController($scope, umbRequestHelper, $log, $http, for (var i in $scope.items) { check($scope.items[i]); } - } -angular.module("umbraco").controller("Umbraco.Dashboard.XmlDataIntegrityReportController", xmlDataIntegrityReportController); \ No newline at end of file + +angular.module("umbraco").controller("Umbraco.Dashboard.XmlDataIntegrityReportController", XmlDataIntegrityReportController); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/xmldataintegrityreport.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/xmldataintegrityreport.html index df235fb254..11f1834ae4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/xmldataintegrityreport.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/xmldataintegrityreport.html @@ -26,6 +26,4 @@
    - -
    diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/settingsdashboardintro.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/settingsdashboardintro.html index fa9849022c..3a45776872 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/settingsdashboardintro.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/settingsdashboardintro.html @@ -9,6 +9,6 @@
  • Download the Editors Manual for details on working with the Umbraco UI
  • Ask a question in the Community Forum
  • Watch our tutorial videos (some are free, some require a subscription)
  • -
  • Find out about our productivity boosting tools and commercial support
  • +
  • Find out about our productivity boosting tools and commercial support
  • Find out about real-life training and certification opportunities
  • diff --git a/src/Umbraco.Web.UI.Client/src/views/media/media.delete.controller.js b/src/Umbraco.Web.UI.Client/src/views/media/media.delete.controller.js index 99f40d14bb..a8be3d0be5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/media.delete.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/media/media.delete.controller.js @@ -10,8 +10,12 @@ function MediaDeleteController($scope, mediaResource, treeService, navigationSer $scope.performDelete = function() { + // stop from firing again on double-click + if ($scope.busy) { return false; } + //mark it for deletion (used in the UI) $scope.currentNode.loading = true; + $scope.busy = true; mediaResource.deleteById($scope.currentNode.id).then(function () { $scope.currentNode.loading = false; @@ -45,6 +49,7 @@ function MediaDeleteController($scope, mediaResource, treeService, navigationSer }, function (err) { $scope.currentNode.loading = false; + $scope.busy = false; //check if response is ysod if (err.status && err.status >= 500) { diff --git a/src/Umbraco.Web.UI.Client/src/views/packager/views/install-local.html b/src/Umbraco.Web.UI.Client/src/views/packager/views/install-local.html index ec8c1e5476..07fb21d00b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/packager/views/install-local.html +++ b/src/Umbraco.Web.UI.Client/src/views/packager/views/install-local.html @@ -69,8 +69,8 @@
    - - + +
    @@ -79,7 +79,7 @@
    diff --git a/src/Umbraco.Web.UI.Client/src/views/packager/views/repo.controller.js b/src/Umbraco.Web.UI.Client/src/views/packager/views/repo.controller.js index dc6dfe7da4..e4afb661e3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/packager/views/repo.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/packager/views/repo.controller.js @@ -31,6 +31,7 @@ vm.closeLightbox = closeLightbox; vm.search = search; + var currSort = "Latest"; //used to cancel any request in progress if another one needs to take it's place var canceler = null; @@ -59,7 +60,7 @@ .then(function(pack) { vm.popular = pack.packages; }), - ourPackageRepositoryResource.search(vm.pagination.pageNumber - 1, vm.pagination.pageSize) + ourPackageRepositoryResource.search(vm.pagination.pageNumber - 1, vm.pagination.pageSize, currSort) .then(function(pack) { vm.packages = pack.packages; vm.pagination.totalPages = Math.ceil(pack.total / vm.pagination.pageSize); @@ -89,12 +90,14 @@ searchCategory = ""; } + currSort = "Latest"; + $q.all([ ourPackageRepositoryResource.getPopular(8, searchCategory) .then(function(pack) { vm.popular = pack.packages; }), - ourPackageRepositoryResource.search(vm.pagination.pageNumber - 1, vm.pagination.pageSize, searchCategory, vm.searchQuery) + ourPackageRepositoryResource.search(vm.pagination.pageNumber - 1, vm.pagination.pageSize, currSort, searchCategory, vm.searchQuery) .then(function(pack) { vm.packages = pack.packages; vm.pagination.totalPages = Math.ceil(pack.total / vm.pagination.pageSize); @@ -132,7 +135,7 @@ } function nextPage(pageNumber) { - ourPackageRepositoryResource.search(pageNumber - 1, vm.pagination.pageSize, getActiveCategory(), vm.searchQuery) + ourPackageRepositoryResource.search(pageNumber - 1, vm.pagination.pageSize, currSort, getActiveCategory(), vm.searchQuery) .then(function (pack) { vm.packages = pack.packages; vm.pagination.totalPages = Math.ceil(pack.total / vm.pagination.pageSize); @@ -140,7 +143,7 @@ } function prevPage(pageNumber) { - ourPackageRepositoryResource.search(pageNumber - 1, vm.pagination.pageSize, getActiveCategory(), vm.searchQuery) + ourPackageRepositoryResource.search(pageNumber - 1, vm.pagination.pageSize, currSort, getActiveCategory(), vm.searchQuery) .then(function (pack) { vm.packages = pack.packages; vm.pagination.totalPages = Math.ceil(pack.total / vm.pagination.pageSize); @@ -148,7 +151,7 @@ } function goToPage(pageNumber) { - ourPackageRepositoryResource.search(pageNumber - 1, vm.pagination.pageSize, getActiveCategory(), vm.searchQuery) + ourPackageRepositoryResource.search(pageNumber - 1, vm.pagination.pageSize, currSort, getActiveCategory(), vm.searchQuery) .then(function (pack) { vm.packages = pack.packages; vm.pagination.totalPages = Math.ceil(pack.total / vm.pagination.pageSize); @@ -248,8 +251,11 @@ canceler = $q.defer(); } + currSort = vm.searchQuery ? "Default" : "Latest"; + ourPackageRepositoryResource.search(vm.pagination.pageNumber - 1, vm.pagination.pageSize, + currSort, "", vm.searchQuery, canceler) diff --git a/src/Umbraco.Web.UI.Client/src/views/packager/views/repo.html b/src/Umbraco.Web.UI.Client/src/views/packager/views/repo.html index 973fa8c440..3975dc96d9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/packager/views/repo.html +++ b/src/Umbraco.Web.UI.Client/src/views/packager/views/repo.html @@ -153,12 +153,13 @@
    - -
    - The package and version chosen is already installed -
    + +
    @@ -198,7 +199,7 @@
    Created:
    -
    {{vm.package.created}}
    +
    {{vm.package.created | date:'yyyy-MM-dd HH:mm:ss'}}
    @@ -312,7 +313,7 @@
    - +

    {{vm.installState.status}}

    diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/radiobuttonlist.html b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/radiobuttonlist.html index a9e07206d7..b82eb88ad2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/radiobuttonlist.html +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/radiobuttonlist.html @@ -1,7 +1,7 @@
    • - +
    \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js index 2c3495120a..e777b0a409 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js @@ -99,7 +99,7 @@ function dateTimePickerController($scope, notificationsService, assetsService, a if (Umbraco.Sys.ServerVariables.application.serverTimeOffset !== undefined) { // Will return something like 120 var serverOffset = Umbraco.Sys.ServerVariables.application.serverTimeOffset; - + // Will return something like -120 var localOffset = new Date().getTimezoneOffset(); @@ -142,8 +142,8 @@ function dateTimePickerController($scope, notificationsService, assetsService, a if ($scope.hasDatetimePickerValue) { var dateVal; //check if we are supposed to offset the time - if ($scope.model.value && $scope.model.config.offsetTime === "1" && Umbraco.Sys.ServerVariables.application.serverTimeOffset) { - //get the local time offset from the server + if ($scope.model.value && $scope.model.config.offsetTime === "1" && $scope.serverTimeNeedsOffsetting) { + //get the local time offset from the server dateVal = dateHelper.convertToLocalMomentTime($scope.model.value, Umbraco.Sys.ServerVariables.application.serverTimeOffset); $scope.serverTime = dateHelper.convertToServerStringTime(dateVal, Umbraco.Sys.ServerVariables.application.serverTimeOffset, "YYYY-MM-DD HH:mm:ss Z"); } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js index 4285cf0f77..df21541f09 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js @@ -71,7 +71,10 @@ function fileUploadController($scope, $element, $compile, imageHelper, fileManag "GetBigThumbnail", [{ originalImagePath: file.file }]); + var extension = file.file.substring(file.file.lastIndexOf(".") + 1, file.file.length); + file.thumbnail = thumbnailUrl; + file.extension = extension.toLowerCase(); }); $scope.clearFiles = false; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.html index c213378b23..0905de07f9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.html @@ -1,10 +1,15 @@ 
    - +