diff --git a/.gitignore b/.gitignore index 7f7fd2bf6e..5a98880c1b 100644 --- a/.gitignore +++ b/.gitignore @@ -71,6 +71,7 @@ src/Umbraco.Web.UI/[Ww]eb.config webpihash.txt node_modules +lib-bower src/Umbraco.Web.UI/[Uu]mbraco/[Ll]ib/* src/Umbraco.Web.UI/[Uu]mbraco/[Jj]s/umbraco.* diff --git a/README.md b/README.md index a9559c1850..a724b3531a 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,7 @@ Umbraco is a free open source Content Management System built on the ASP.NET pla ## Building Umbraco from source ## -The easiest way to get started is to run `build/build.bat` which will build both the backoffice (also known as "Belle") and the Umbraco core. You can then easily start debugging from Visual Studio, or if you need to debug Belle you can run `grunt vs` in `src\Umbraco.Web.UI.Client`. - -If you're interested in making changes to Belle without running Visual Studio make sure to read the [Belle ReadMe file](src/Umbraco.Web.UI.Client/README.md). +The easiest way to get started is to run `build/build.bat` which will build both the backoffice (also known as "Belle") and the Umbraco core. You can then easily start debugging from Visual Studio, or if you need to debug Belle you can run `gulp dev` in `src\Umbraco.Web.UI.Client`. Note that you can always [download a nightly build](http://nightly.umbraco.org/?container=umbraco-750) so you don't have to build the code yourself. diff --git a/appveyor.yml b/appveyor.yml index dc6e22edbf..4c9a33fd23 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,5 +1,9 @@ version: '{build}' shallow_clone: true + +init: + - ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) + build_script: - cmd: >- SET SLN=%CD% diff --git a/build/BuildBelle.bat b/build/BuildBelle.bat index 750ffae6b2..cc6ce2202c 100644 --- a/build/BuildBelle.bat +++ b/build/BuildBelle.bat @@ -5,8 +5,10 @@ SETLOCAL SET toolsFolder=%CD%\tools\ ECHO Current folder: %CD% -SET nodeFileName=node-v6.9.1-win-x86.7z -SET nodeExtractFolder=%toolsFolder%node.js.691 +SET nodeVersion=6.11.2 + +SET nodeFileName=node-v%nodeVersion%-win-x86.7z +SET nodeExtractFolder=%toolsFolder%node.js.%nodeVersion% SET nuGetExecutable=%CD%\tools\nuget.exe IF NOT EXIST "%nuGetExecutable%" ( @@ -25,44 +27,42 @@ FOR /f "delims=" %%A in ('dir "%toolsFolder%7-Zip.CommandLine.*" /b') DO SET "se MOVE "%sevenZipExePath%tools\7za.exe" "%toolsFolder%7za.exe" IF NOT EXIST "%nodeExtractFolder%" ( - ECHO Downloading http://nodejs.org/dist/v6.9.1/%nodeFileName% to %toolsFolder%%nodeFileName% - powershell -Command "(New-Object Net.WebClient).DownloadFile('http://nodejs.org/dist/v6.9.1/%nodeFileName%', '%toolsFolder%%nodeFileName%')" + ECHO Downloading http://nodejs.org/dist/v%nodeVersion%/%nodeFileName% to %toolsFolder%%nodeFileName% + powershell -Command "(New-Object Net.WebClient).DownloadFile('http://nodejs.org/dist/v%nodeVersion%/%nodeFileName%', '%toolsFolder%%nodeFileName%')" ECHO Extracting %nodeFileName% to %nodeExtractFolder% "%toolsFolder%\7za.exe" x "%toolsFolder%\%nodeFileName%" -o"%nodeExtractFolder%" -aos > nul ) FOR /f "delims=" %%A in ('dir "%nodeExtractFolder%\node*" /b') DO SET "nodePath=%nodeExtractFolder%\%%A" - -SET drive=%CD:~0,2% -SET nuGetFolder=%drive%\packages\ -FOR /f "delims=" %%A in ('dir "%nuGetFolder%npm.*" /b') DO SET "npmPath=%nuGetFolder%%%A\" - -IF [%npmPath%] == [] GOTO :installnpm -IF NOT [%npmPath%] == [] GOTO :build - -:installnpm - ECHO Downloading npm - ECHO Configured packages folder: %nuGetFolder% - ECHO Installing Npm NuGet Package - "%nuGetExecutable%" install Npm -OutputDirectory %nuGetFolder% -Verbosity detailed - REM Ensures that we look for the just downloaded NPM, not whatever the user has installed on their machine - FOR /f "delims=" %%A in ('dir %nuGetFolder%npm.* /b') DO SET "npmPath=%nuGetFolder%%%A\" - GOTO :build - + :build - ECHO Adding Npm and Node to path + ECHO Adding Node to path REM SETLOCAL is on, so changes to the path not persist to the actual user's path - PATH="%npmPath%";"%nodePath%";%PATH% + PATH="%nodePath%";%PATH% + + ECHO Node version is: + call node -v + + ECHO npm version is: + call npm -v + SET buildFolder=%CD% ECHO Change directory to %CD%\..\src\Umbraco.Web.UI.Client\ CD %CD%\..\src\Umbraco.Web.UI.Client\ - ECHO Do npm install and the grunt build of Belle + ECHO Do npm install and the gulp build of Belle + + ECHO Clean npm cache call npm cache clean --quiet - call npm install --quiet - call npm install -g grunt-cli --quiet + + ECHO Installing gulp cli + call npm install -g gulp-cli --quiet + ECHO Installing bower call npm install -g bower --quiet - call grunt build --buildversion=%release% + ECHO Doing npm install + call npm install --quiet + ECHO Executing gulp build + call gulp build --buildversion=%release% ECHO Move back to the build folder CD "%buildFolder%" \ No newline at end of file diff --git a/build/UmbracoVersion.txt b/build/UmbracoVersion.txt index ae27482bf3..6f8b223965 100644 --- a/build/UmbracoVersion.txt +++ b/build/UmbracoVersion.txt @@ -1,3 +1,3 @@ # Usage: on line 2 put the release version, on line 3 put the version comment (example: beta) 7.7.0 -beta003 \ No newline at end of file +beta005 \ No newline at end of file diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 2256adc07d..65df52011e 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -12,4 +12,4 @@ using System.Resources; [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyFileVersion("7.7.0")] -[assembly: AssemblyInformationalVersion("7.7.0-beta003")] \ No newline at end of file +[assembly: AssemblyInformationalVersion("7.7.0-beta005")] \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs b/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs index 389c620637..3026064dec 100644 --- a/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs +++ b/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs @@ -9,7 +9,7 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Core.Configuration.Grid { - class GridEditorsConfig : IGridEditorsConfig + internal class GridEditorsConfig : IGridEditorsConfig { private readonly ILogger _logger; private readonly IRuntimeCacheProvider _runtimeCache; diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/ISecuritySection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/ISecuritySection.cs index 4cc9ba402a..31ee4611d0 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/ISecuritySection.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/ISecuritySection.cs @@ -13,6 +13,15 @@ string AuthCookieName { get; } - string AuthCookieDomain { get; } + string AuthCookieDomain { get; } + + /// + /// A boolean indicating that by default the email address will be the username + /// + /// + /// Even if this is true and the username is different from the email in the database, the username field will still be shown. + /// When this is false, the username and email fields will be shown in the user section. + /// + bool UsernameIsEmail { get; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/SecurityElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/SecurityElement.cs index 1918d740ba..dc2ba7e983 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/SecurityElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/SecurityElement.cs @@ -25,6 +25,19 @@ namespace Umbraco.Core.Configuration.UmbracoSettings get { return GetOptionalTextElement("allowPasswordReset", true); } } + /// + /// A boolean indicating that by default the email address will be the username + /// + /// + /// Even if this is true and the username is different from the email in the database, the username field will still be shown. + /// When this is false, the username and email fields will be shown in the user section. + /// + [ConfigurationProperty("usernameIsEmail")] + internal InnerTextConfigurationElement UsernameIsEmail + { + get { return GetOptionalTextElement("usernameIsEmail", true); } + } + [ConfigurationProperty("authCookieName")] internal InnerTextConfigurationElement AuthCookieName { @@ -55,6 +68,18 @@ namespace Umbraco.Core.Configuration.UmbracoSettings get { return AllowPasswordReset; } } + /// + /// A boolean indicating that by default the email address will be the username + /// + /// + /// Even if this is true and the username is different from the email in the database, the username field will still be shown. + /// When this is false, the username and email fields will be shown in the user section. + /// + bool ISecuritySection.UsernameIsEmail + { + get { return UsernameIsEmail; } + } + string ISecuritySection.AuthCookieName { get { return AuthCookieName; } diff --git a/src/Umbraco.Core/Configuration/UmbracoVersion.cs b/src/Umbraco.Core/Configuration/UmbracoVersion.cs index ac5375bfe9..2b51d86830 100644 --- a/src/Umbraco.Core/Configuration/UmbracoVersion.cs +++ b/src/Umbraco.Core/Configuration/UmbracoVersion.cs @@ -24,7 +24,7 @@ namespace Umbraco.Core.Configuration /// Gets the version comment (like beta or RC). /// /// The version comment. - public static string CurrentComment { get { return "beta003"; } } + public static string CurrentComment { get { return "beta005"; } } // Get the version of the umbraco.dll by looking at a class in that dll // Had to do it like this due to medium trust issues, see: http://haacked.com/archive/2010/11/04/assembly-location-and-medium-trust.aspx diff --git a/src/Umbraco.Core/Constants-ObjectTypes.cs b/src/Umbraco.Core/Constants-ObjectTypes.cs index 18d7ccc7be..059b1cbd3c 100644 --- a/src/Umbraco.Core/Constants-ObjectTypes.cs +++ b/src/Umbraco.Core/Constants-ObjectTypes.cs @@ -232,6 +232,16 @@ namespace Umbraco.Core /// Guid for a Forms DataSource. /// public static readonly Guid LanguageGuid = new Guid(Language); + + /// + /// Guid for an Identifier Reservation. + /// + public const string IdReservation = "92849B1E-3904-4713-9356-F646F87C25F4"; + + /// + /// Guid for an Identifier Reservation. + /// + public static readonly Guid IdReservationGuid = new Guid(IdReservation); } } } \ No newline at end of file diff --git a/src/Umbraco.Core/CoreBootManager.cs b/src/Umbraco.Core/CoreBootManager.cs index dcce15f419..a0455d820a 100644 --- a/src/Umbraco.Core/CoreBootManager.cs +++ b/src/Umbraco.Core/CoreBootManager.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Configuration; using System.IO; using System.Linq; +using System.Text.RegularExpressions; using System.Threading; using System.Web; using AutoMapper; @@ -399,6 +400,28 @@ namespace Umbraco.Core if (ApplicationContext.IsConfigured == false) return; if (ApplicationContext.DatabaseContext.IsDatabaseConfigured == false) return; + // deal with localdb + var databaseContext = ApplicationContext.DatabaseContext; + var localdbex = new Regex(@"\(localdb\)\\([a-zA-Z0-9-_]+)(;|$)"); + var m = localdbex.Match(databaseContext.ConnectionString); + if (m.Success) + { + var instanceName = m.Groups[1].Value; + ProfilingLogger.Logger.Info(string.Format("LocalDb instance \"{0}\"", instanceName)); + + var localDb = new LocalDb(); + if (localDb.IsAvailable == false) + throw new UmbracoStartupFailedException("Umbraco cannot start. LocalDb is not available."); + + if (localDb.InstanceExists(m.Groups[1].Value) == false) + { + if (localDb.CreateInstance(instanceName) == false) + throw new UmbracoStartupFailedException(string.Format("Umbraco cannot start. LocalDb cannot create instance \"{0}\".", instanceName)); + if (localDb.StartInstance(instanceName) == false) + throw new UmbracoStartupFailedException(string.Format("Umbraco cannot start. LocalDb cannot start instance \"{0}\".", instanceName)); + } + } + //try now if (ApplicationContext.DatabaseContext.CanConnect) return; diff --git a/src/Umbraco.Core/Deploy/IDeployContext.cs b/src/Umbraco.Core/Deploy/IDeployContext.cs index 7d4066e015..531ed9dae4 100644 --- a/src/Umbraco.Core/Deploy/IDeployContext.cs +++ b/src/Umbraco.Core/Deploy/IDeployContext.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading; namespace Umbraco.Core.Deploy { @@ -38,5 +39,10 @@ namespace Umbraco.Core.Deploy /// The key of the item. /// The item with the specified key and type, if any, else null. T Item(string key) where T : class; + + ///// + ///// Gets the global deployment cancellation token. + ///// + //CancellationToken CancellationToken { get; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/EmailSender.cs b/src/Umbraco.Core/EmailSender.cs new file mode 100644 index 0000000000..6f381f693c --- /dev/null +++ b/src/Umbraco.Core/EmailSender.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Net.Mail; +using System.Text; +using System.Threading.Tasks; +using System.Web; +using System.Web.Routing; +using Umbraco.Core.Configuration; +using Umbraco.Core.Events; + +namespace Umbraco.Core +{ + /// + /// A utility class for sending emails + /// + public class EmailSender : IEmailSender + { + //TODO: This should encapsulate a BackgroundTaskRunner with a queue to send these emails! + + private readonly bool _enableEvents; + + /// + /// Default constructor + /// + public EmailSender() : this(false) + { + } + + internal EmailSender(bool enableEvents) + { + _enableEvents = enableEvents; + } + + private static readonly Lazy SmtpConfigured = new Lazy(() => GlobalSettings.HasSmtpServerConfigured(HttpRuntime.AppDomainAppVirtualPath)); + + /// + /// Sends the message non-async + /// + /// + public void Send(MailMessage message) + { + if (SmtpConfigured.Value == false && _enableEvents) + { + OnSendEmail(new SendEmailEventArgs(message)); + } + else + { + using (var client = new SmtpClient()) + { + client.Send(message); + } + } + } + + /// + /// Sends the message async + /// + /// + /// + public async Task SendAsync(MailMessage message) + { + if (SmtpConfigured.Value == false && _enableEvents) + { + OnSendEmail(new SendEmailEventArgs(message)); + } + else + { + using (var client = new SmtpClient()) + { + if (client.DeliveryMethod == SmtpDeliveryMethod.Network) + { + await client.SendMailAsync(message); + } + else + { + client.Send(message); + } + } + } + } + + /// + /// Returns true if the application should be able to send a required application email + /// + /// + /// We assume this is possible if either an event handler is registered or an smtp server is configured + /// + internal static bool CanSendRequiredEmail + { + get { return EventHandlerRegistered || SmtpConfigured.Value; } + } + + /// + /// returns true if an event handler has been registered + /// + internal static bool EventHandlerRegistered + { + get { return SendEmail != null; } + } + + /// + /// An event that is raised when no smtp server is configured if events are enabled + /// + internal static event EventHandler SendEmail; + + private static void OnSendEmail(SendEmailEventArgs e) + { + var handler = SendEmail; + if (handler != null) handler(null, e); + } + } +} diff --git a/src/Umbraco.Core/EnumerableExtensions.cs b/src/Umbraco.Core/EnumerableExtensions.cs index 79a703ca5f..81c0df793e 100644 --- a/src/Umbraco.Core/EnumerableExtensions.cs +++ b/src/Umbraco.Core/EnumerableExtensions.cs @@ -298,5 +298,16 @@ namespace Umbraco.Core return list1Groups.Count == list2Groups.Count && list1Groups.All(g => g.Count() == list2Groups[g.Key].Count()); } + + public static IEnumerable SkipLast(this IEnumerable source) + { + using (var e = source.GetEnumerator()) + { + if (e.MoveNext() == false) yield break; + + for (var value = e.Current; e.MoveNext(); value = e.Current) + yield return value; + } + } } } diff --git a/src/Umbraco.Core/Events/SendEmailEventArgs.cs b/src/Umbraco.Core/Events/SendEmailEventArgs.cs new file mode 100644 index 0000000000..8c6a2138d5 --- /dev/null +++ b/src/Umbraco.Core/Events/SendEmailEventArgs.cs @@ -0,0 +1,15 @@ +using System; +using System.Net.Mail; + +namespace Umbraco.Core.Events +{ + internal class SendEmailEventArgs : EventArgs + { + public MailMessage Message { get; private set; } + + public SendEmailEventArgs(MailMessage message) + { + Message = message; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/IEmailSender.cs b/src/Umbraco.Core/IEmailSender.cs new file mode 100644 index 0000000000..7e6565a53b --- /dev/null +++ b/src/Umbraco.Core/IEmailSender.cs @@ -0,0 +1,13 @@ +using System.Net.Mail; +using System.Threading.Tasks; + +namespace Umbraco.Core +{ + /// + /// Simple abstraction to send an email message + /// + public interface IEmailSender + { + Task SendAsync(MailMessage message); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Membership/User.cs b/src/Umbraco.Core/Models/Membership/User.cs index d1270ad050..e01b070d18 100644 --- a/src/Umbraco.Core/Models/Membership/User.cs +++ b/src/Umbraco.Core/Models/Membership/User.cs @@ -17,7 +17,7 @@ namespace Umbraco.Core.Models.Membership /// [Serializable] [DataContract(IsReference = true)] - public class User : Entity, IUser + public class User : Entity, IUser, IProfile { /// /// Constructor for creating a new/empty user diff --git a/src/Umbraco.Core/Models/UmbracoObjectTypes.cs b/src/Umbraco.Core/Models/UmbracoObjectTypes.cs index 3a196dda00..c7ad5eb2aa 100644 --- a/src/Umbraco.Core/Models/UmbracoObjectTypes.cs +++ b/src/Umbraco.Core/Models/UmbracoObjectTypes.cs @@ -193,6 +193,14 @@ namespace Umbraco.Core.Models [UmbracoObjectType(Constants.ObjectTypes.DocumentBlueprint, typeof(IContent))] [FriendlyName("DocumentBlueprint")] [UmbracoUdiType(Constants.UdiEntityType.DocumentBluePrint)] - DocumentBlueprint + DocumentBlueprint, + + /// + /// Reserved Identifier + /// + [UmbracoObjectType(Constants.ObjectTypes.IdReservation)] + [FriendlyName("Identifier Reservation")] + IdReservation + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/LocalDb.cs b/src/Umbraco.Core/Persistence/LocalDb.cs new file mode 100644 index 0000000000..f706979e17 --- /dev/null +++ b/src/Umbraco.Core/Persistence/LocalDb.cs @@ -0,0 +1,959 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.SqlClient; +using System.Diagnostics; +using System.IO; +using System.Linq; + +namespace Umbraco.Core.Persistence +{ + /// + /// Manages LocalDB databases. + /// + /// + /// Latest version is SQL Server 2016 Express LocalDB, + /// see https://docs.microsoft.com/en-us/sql/database-engine/configure-windows/sql-server-2016-express-localdb + /// which can be installed by downloading the Express installer from https://www.microsoft.com/en-us/sql-server/sql-server-downloads + /// (about 5MB) then select 'download media' to download SqlLocalDB.msi (about 44MB), which you can execute. This installs + /// LocalDB only. Though you probably want to install the full Express. You may also want to install SQL Server Management + /// Studio which can be used to connect to LocalDB databases. + /// See also https://github.com/ritterim/automation-sql which is a somewhat simpler version of this. + /// + internal class LocalDb + { + private int _version; + private bool _hasVersion; + + #region Availability & Version + + /// + /// Gets the LocalDb installed version. + /// + /// If more than one version is installed, returns the highest available. Returns + /// the major version as an integer e.g. 11, 12... + /// Thrown when LocalDb is not available. + public int Version + { + get + { + EnsureVersion(); + if (_version <= 0) + throw new InvalidOperationException("LocalDb is not available."); + return _version; + } + } + + /// + /// Ensures that the LocalDb version is detected. + /// + private void EnsureVersion() + { + if (_hasVersion) return; + DetectVersion(); + _hasVersion = true; + } + + /// + /// Gets a value indicating whether LocalDb is available. + /// + public bool IsAvailable + { + get + { + EnsureVersion(); + return _version > 0; + } + } + + /// + /// Ensures that LocalDb is available. + /// + /// Thrown when LocalDb is not available. + private void EnsureAvailable() + { + if (IsAvailable == false) + throw new InvalidOperationException("LocalDb is not available."); + } + + /// + /// Detects LocalDb installed version. + /// + /// If more than one version is installed, the highest available is detected. + private void DetectVersion() + { + _hasVersion = true; + _version = -1; + + var programFiles = Environment.GetEnvironmentVariable("ProgramFiles"); + if (programFiles == null) return; + + // detect 14, 13, 12, 11 + for (var i = 14; i > 10; i--) + { + var path = Path.Combine(programFiles, string.Format(@"Microsoft SQL Server\{0}0\Tools\Binn\SqlLocalDB.exe", i)); + if (File.Exists(path) == false) continue; + _version = i; + break; + } + } + + #endregion + + #region Instances + + /// + /// Gets the name of existing LocalDb instances. + /// + /// The name of existing LocalDb instances. + /// Thrown when LocalDb is not available. + public string[] GetInstances() + { + EnsureAvailable(); + string output, error; + var rc = ExecuteSqlLocalDb("i", out output, out error); // info + if (rc != 0 || error != string.Empty) return null; + return output.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); + } + + /// + /// Gets a value indicating whether a LocalDb instance exists. + /// + /// The name of the instance. + /// A value indicating whether a LocalDb instance with the specified name exists. + /// Thrown when LocalDb is not available. + public bool InstanceExists(string instanceName) + { + EnsureAvailable(); + var instances = GetInstances(); + return instances != null && instances.Contains(instanceName); + } + + /// + /// Creates a LocalDb instance. + /// + /// The name of the instance. + /// A value indicating whether the instance was created without errors. + /// Thrown when LocalDb is not available. + public bool CreateInstance(string instanceName) + { + EnsureAvailable(); + string output, error; + return ExecuteSqlLocalDb(string.Format("c \"{0}\"", instanceName), out output, out error) == 0 && error == string.Empty; + } + + /// + /// Drops a LocalDb instance. + /// + /// The name of the instance. + /// A value indicating whether the instance was dropped without errors. + /// Thrown when LocalDb is not available. + /// + /// When an instance is dropped all the attached database files are deleted. + /// Successful if the instance does not exist. + /// + public bool DropInstance(string instanceName) + { + EnsureAvailable(); + var instance = GetInstance(instanceName); + if (instance == null) return true; + instance.DropDatabases(); // else the files remain + + // -i force NOWAIT, -k kills + string output, error; + return ExecuteSqlLocalDb(string.Format("p \"{0}\" -i", instanceName), out output, out error) == 0 && error == string.Empty + && ExecuteSqlLocalDb(string.Format("d \"{0}\"", instanceName), out output, out error) == 0 && error == string.Empty; + } + + /// + /// Stops a LocalDb instance. + /// + /// The name of the instance. + /// A value indicating whether the instance was stopped without errors. + /// Thrown when LocalDb is not available. + /// + /// Successful if the instance does not exist. + /// + public bool StopInstance(string instanceName) + { + EnsureAvailable(); + if (InstanceExists(instanceName) == false) return true; + + // -i force NOWAIT, -k kills + string output, error; + return ExecuteSqlLocalDb(string.Format("p \"{0}\" -i", instanceName), out output, out error) == 0 && error == string.Empty; + } + + /// + /// Stops a LocalDb instance. + /// + /// The name of the instance. + /// A value indicating whether the instance was started without errors. + /// Thrown when LocalDb is not available. + /// + /// Failed if the instance does not exist. + /// + public bool StartInstance(string instanceName) + { + EnsureAvailable(); + if (InstanceExists(instanceName) == false) return false; + string output, error; + return ExecuteSqlLocalDb(string.Format("s \"{0}\"", instanceName), out output, out error) == 0 && error == string.Empty; + } + + /// + /// Gets a LocalDb instance. + /// + /// The name of the instance. + /// The instance with the specified name if it exists, otherwise null. + /// Thrown when LocalDb is not available. + public Instance GetInstance(string instanceName) + { + EnsureAvailable(); + return InstanceExists(instanceName) ? new Instance(instanceName) : null; + } + + #endregion + + #region Databases + + /// + /// Represents a LocalDb instance. + /// + /// + /// LocalDb is assumed to be available, and the instance is assumed to exist. + /// + public class Instance + { + private readonly string _masterCstr; + + /// + /// Gets the name of the instance. + /// + public string InstanceName { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + /// + public Instance(string instanceName) + { + InstanceName = instanceName; + _masterCstr = string.Format(@"Server=(localdb)\{0};Integrated Security=True;", instanceName); + } + + /// + /// Gets a LocalDb connection string. + /// + /// The name of the database. + /// The connection string for the specified database. + /// + /// The database should exist in the LocalDb instance. + /// + public string GetConnectionString(string databaseName) + { + return _masterCstr + string.Format(@"Database={0};", databaseName); + } + + /// + /// Gets a LocalDb connection string for an attached database. + /// + /// The name of the database. + /// The directory containing database files. + /// The connection string for the specified database. + /// + /// The database should not exist in the LocalDb instance. + /// It will be attached with its name being its MDF filename (full path), uppercased, when + /// the first connection is opened, and remain attached until explicitely detached. + /// + public string GetAttachedConnectionString(string databaseName, string filesPath) + { + string logName, baseFilename, baseLogFilename, mdfFilename, ldfFilename; + GetDatabaseFiles(databaseName, filesPath, out logName, out baseFilename, out baseLogFilename, out mdfFilename, out ldfFilename); + + return _masterCstr + string.Format(@"AttachDbFileName='{0}';", mdfFilename); + } + + /// + /// Gets the name of existing databases. + /// + /// The name of existing databases. + public string[] GetDatabases() + { + var userDatabases = new List(); + + using (var conn = new SqlConnection(_masterCstr)) + using (var cmd = conn.CreateCommand()) + { + conn.Open(); + + var databases = new Dictionary(); + + SetCommand(cmd, @" + SELECT name, filename FROM sys.sysdatabases"); + + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + databases[reader.GetString(0)] = reader.GetString(1); + } + } + + foreach (var database in databases) + { + var dbname = database.Key; + + if (dbname == "master" || dbname == "tempdb" || dbname == "model" || dbname == "msdb") + continue; + + // fixme - shall we deal with stale databases? + // fixme - is it always ok to assume file names? + //var mdf = database.Value; + //var ldf = mdf.Replace(".mdf", "_log.ldf"); + //if (staleOnly && File.Exists(mdf) && File.Exists(ldf)) + // continue; + + //ExecuteDropDatabase(cmd, dbname, mdf, ldf); + //count++; + + userDatabases.Add(dbname); + } + } + + return userDatabases.ToArray(); + } + + /// + /// Gets a value indicating whether a database exists. + /// + /// The name of the database. + /// A value indicating whether a database with the specified name exists. + /// + /// A database exists if it is registered in the instance, and its files exist. If the database + /// is registered but some of its files are missing, the database is dropped. + /// + public bool DatabaseExists(string databaseName) + { + using (var conn = new SqlConnection(_masterCstr)) + using (var cmd = conn.CreateCommand()) + { + conn.Open(); + + var mdf = GetDatabase(cmd, databaseName); + if (mdf == null) return false; + + // it can exist, even though its files have been deleted + // if files exist assume all is ok (should we try to connect?) + var ldf = GetLogFilename(mdf); + if (File.Exists(mdf) && File.Exists(ldf)) + return true; + + ExecuteDropDatabase(cmd, databaseName, mdf, ldf); + } + + return false; + } + + /// + /// Creates a new database. + /// + /// The name of the database. + /// The directory containing database files. + /// A value indicating whether the database was created without errors. + /// + /// Failed if a database with the specified name already exists in the instance, + /// or if the database files already exist in the specified directory. + /// + public bool CreateDatabase(string databaseName, string filesPath) + { + string logName, baseFilename, baseLogFilename, mdfFilename, ldfFilename; + GetDatabaseFiles(databaseName, filesPath, out logName, out baseFilename, out baseLogFilename, out mdfFilename, out ldfFilename); + + using (var conn = new SqlConnection(_masterCstr)) + using (var cmd = conn.CreateCommand()) + { + conn.Open(); + + var mdf = GetDatabase(cmd, databaseName); + if (mdf != null) return false; + + // cannot use parameters on CREATE DATABASE + // ie "CREATE DATABASE @0 ..." does not work + SetCommand(cmd, string.Format(@" + CREATE DATABASE {0} + ON (NAME=N{1}, FILENAME={2}) + LOG ON (NAME=N{3}, FILENAME={4})", + QuotedName(databaseName), + QuotedName(databaseName, '\''), QuotedName(mdfFilename, '\''), + QuotedName(logName, '\''), QuotedName(ldfFilename, '\''))); + + var unused = cmd.ExecuteNonQuery(); + } + return true; + } + + /// + /// Drops a database. + /// + /// The name of the database. + /// A value indicating whether the database was dropped without errors. + /// + /// Successful if the database does not exist. + /// Deletes the database files. + /// + public bool DropDatabase(string databaseName) + { + using (var conn = new SqlConnection(_masterCstr)) + using (var cmd = conn.CreateCommand()) + { + conn.Open(); + + SetCommand(cmd, @" + SELECT name, filename FROM master.dbo.sysdatabases WHERE ('[' + name + ']' = @0 OR name = @0)", + databaseName); + + var mdf = GetDatabase(cmd, databaseName); + if (mdf == null) return true; + + ExecuteDropDatabase(cmd, databaseName, mdf); + } + + return true; + } + + /// + /// Drops stale databases. + /// + /// The number of databases that were dropped. + /// + /// A database is considered stale when its files cannot be found. + /// + public int DropStaleDatabases() + { + return DropDatabases(true); + } + + /// + /// Drops databases. + /// + /// A value indicating whether to delete only stale database. + /// The number of databases that were dropped. + /// + /// A database is considered stale when its files cannot be found. + /// + public int DropDatabases(bool staleOnly = false) + { + var count = 0; + using (var conn = new SqlConnection(_masterCstr)) + using (var cmd = conn.CreateCommand()) + { + conn.Open(); + + var databases = new Dictionary(); + + SetCommand(cmd, @" + SELECT name, filename FROM sys.sysdatabases"); + + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + databases[reader.GetString(0)] = reader.GetString(1); + } + } + + foreach (var database in databases) + { + var dbname = database.Key; + + if (dbname == "master" || dbname == "tempdb" || dbname == "model" || dbname == "msdb") + continue; + + var mdf = database.Value; + var ldf = mdf.Replace(".mdf", "_log.ldf"); + if (staleOnly && File.Exists(mdf) && File.Exists(ldf)) + continue; + + ExecuteDropDatabase(cmd, dbname, mdf, ldf); + count++; + } + } + + return count; + } + + /// + /// Detaches a database. + /// + /// The name of the database. + /// The directory containing the database files. + /// Thrown when a database with the specified name does not exist. + public string DetachDatabase(string databaseName) + { + using (var conn = new SqlConnection(_masterCstr)) + using (var cmd = conn.CreateCommand()) + { + conn.Open(); + + var mdf = GetDatabase(cmd, databaseName); + if (mdf == null) + throw new InvalidOperationException("Database does not exist."); + + DetachDatabase(cmd, databaseName); + + return Path.GetDirectoryName(mdf); + } + } + + /// + /// Attaches a database. + /// + /// The name of the database. + /// The directory containing database files. + /// Thrown when a database with the specified name already exists. + public void AttachDatabase(string databaseName, string filesPath) + { + using (var conn = new SqlConnection(_masterCstr)) + using (var cmd = conn.CreateCommand()) + { + conn.Open(); + + var mdf = GetDatabase(cmd, databaseName); + if (mdf != null) + throw new InvalidOperationException("Database already exists."); + + AttachDatabase(cmd, databaseName, filesPath); + } + } + + /// + /// Gets the file names of a database. + /// + /// The name of the database. + /// The MDF logical name. + /// The LDF logical name. + /// The MDF filename. + /// The LDF filename. + public void GetFilenames(string databaseName, + out string mdfName, out string ldfName, + out string mdfFilename, out string ldfFilename) + { + using (var conn = new SqlConnection(_masterCstr)) + using (var cmd = conn.CreateCommand()) + { + conn.Open(); + + GetFilenames(cmd, databaseName, out mdfName, out ldfName, out mdfFilename, out ldfFilename); + } + } + + /// + /// Kills all existing connections. + /// + /// The name of the database. + public void KillConnections(string databaseName) + { + using (var conn = new SqlConnection(_masterCstr)) + using (var cmd = conn.CreateCommand()) + { + conn.Open(); + + SetCommand(cmd, @" + DECLARE @sql VARCHAR(MAX); + SELECT @sql = COALESCE(@sql,'') + 'kill ' + CONVERT(VARCHAR, SPId) + ';' + FROM master.sys.sysprocesses + WHERE DBId = DB_ID(@0) AND SPId <> @@SPId; + EXEC(@sql);", + databaseName); + cmd.ExecuteNonQuery(); + } + } + + /// + /// Gets a database. + /// + /// The Sql Command. + /// The name of the database. + /// The full filename of the MDF file, if the database exists, otherwise null. + private static string GetDatabase(SqlCommand cmd, string databaseName) + { + SetCommand(cmd, @" + SELECT name, filename FROM master.dbo.sysdatabases WHERE ('[' + name + ']' = @0 OR name = @0)", + databaseName); + + string mdf = null; + using (var reader = cmd.ExecuteReader()) + { + if (reader.Read()) + mdf = reader.GetString(1) ?? string.Empty; + while (reader.Read()) + { + } + } + + return mdf; + } + + /// + /// Drops a database and its files. + /// + /// The Sql command. + /// The name of the database. + /// The name of the database (MDF) file. + /// The name of the log (LDF) file. + private static void ExecuteDropDatabase(SqlCommand cmd, string databaseName, string mdf, string ldf = null) + { + try + { + // cannot use parameters on ALTER DATABASE + // ie "ALTER DATABASE @0 ..." does not work + SetCommand(cmd, string.Format(@" + ALTER DATABASE {0} SET SINGLE_USER WITH ROLLBACK IMMEDIATE", + QuotedName(databaseName))); + + var unused1 = cmd.ExecuteNonQuery(); + } + catch (SqlException e) + { + if (e.Message.Contains("Unable to open the physical file") && e.Message.Contains("Operating system error 2:")) + { + // quite probably, the files were missing + // yet, it should be possible to drop the database anyways + // but we'll have to deal with the files + } + else + { + // no idea, throw + throw; + } + } + + // cannot use parameters on DROP DATABASE + // ie "DROP DATABASE @0 ..." does not work + SetCommand(cmd, string.Format(@" + DROP DATABASE {0}", + QuotedName(databaseName))); + + var unused2 = cmd.ExecuteNonQuery(); + + // be absolutely sure + if (File.Exists(mdf)) File.Delete(mdf); + ldf = ldf ?? GetLogFilename(mdf); + if (File.Exists(ldf)) File.Delete(ldf); + } + + /// + /// Gets the log (LDF) filename corresponding to a database (MDF) filename. + /// + /// The MDF filename. + /// + private static string GetLogFilename(string mdfFilename) + { + if (mdfFilename.EndsWith(".mdf") == false) + throw new ArgumentException("Not a valid MDF filename (no .mdf extension).", "mdfFilename"); + return mdfFilename.Substring(0, mdfFilename.Length - ".mdf".Length) + "_log.ldf"; + } + + /// + /// Detaches a database. + /// + /// The Sql command. + /// The name of the database. + private static void DetachDatabase(SqlCommand cmd, string databaseName) + { + // cannot use parameters on ALTER DATABASE + // ie "ALTER DATABASE @0 ..." does not work + SetCommand(cmd, string.Format(@" + ALTER DATABASE {0} SET SINGLE_USER WITH ROLLBACK IMMEDIATE", + QuotedName(databaseName))); + + var unused1 = cmd.ExecuteNonQuery(); + + SetCommand(cmd, @" + EXEC sp_detach_db @dbname=@0", + databaseName); + + var unused2 = cmd.ExecuteNonQuery(); + } + + /// + /// Attaches a database. + /// + /// The Sql command. + /// The name of the database. + /// The directory containing database files. + private static void AttachDatabase(SqlCommand cmd, string databaseName, string filesPath) + { + string logName, baseFilename, baseLogFilename, mdfFilename, ldfFilename; + GetDatabaseFiles(databaseName, filesPath, out logName, out baseFilename, out baseLogFilename, out mdfFilename, out ldfFilename); + + // cannot use parameters on CREATE DATABASE + // ie "CREATE DATABASE @0 ..." does not work + SetCommand(cmd, string.Format(@" + CREATE DATABASE {0} + ON (NAME=N{1}, FILENAME={2}) + LOG ON (NAME=N{3}, FILENAME={4}) + FOR ATTACH", + QuotedName(databaseName), + QuotedName(databaseName, '\''), QuotedName(mdfFilename, '\''), + QuotedName(logName, '\''), QuotedName(ldfFilename, '\''))); + + var unused = cmd.ExecuteNonQuery(); + } + + /// + /// Sets a database command. + /// + /// The command. + /// The command text. + /// The command arguments. + /// + /// The command text must refer to arguments as @0, @1... each referring + /// to the corresponding position in . + /// + private static void SetCommand(SqlCommand cmd, string sql, params object[] args) + { + cmd.CommandType = CommandType.Text; + cmd.CommandText = sql; + cmd.Parameters.Clear(); + for (var i = 0; i < args.Length; i++) + cmd.Parameters.AddWithValue("@" + i, args[i]); + } + + /// + /// Gets the file names of a database. + /// + /// The Sql command. + /// The name of the database. + /// The MDF logical name. + /// The LDF logical name. + /// The MDF filename. + /// The LDF filename. + private void GetFilenames(SqlCommand cmd, string databaseName, + out string mdfName, out string ldfName, + out string mdfFilename, out string ldfFilename) + { + mdfName = ldfName = mdfFilename = ldfFilename = null; + + SetCommand(cmd, @" + SELECT DB_NAME(database_id), type_desc, name, physical_name + FROM master.sys.master_files + WHERE database_id=DB_ID(@0)", + databaseName); + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + var type = reader.GetString(1); + if (type == "ROWS") + { + mdfName = reader.GetString(2); + ldfName = reader.GetString(3); + } + else if (type == "LOG") + { + ldfName = reader.GetString(2); + ldfFilename = reader.GetString(3); + } + } + } + } + } + + /// + /// Copy database files. + /// + /// The name of the source database. + /// The directory containing source database files. + /// The name of the target database. + /// The directory containing target database files. + /// The source database files extension. + /// The target database files extension. + /// A value indicating whether to overwrite the target files. + /// A value indicating whether to delete the source files. + /// + /// The , , + /// and parameters are optional. If they result in target being identical + /// to source, no copy is performed. If is false, nothing happens, otherwise the source + /// files are deleted. + /// If target is not identical to source, files are copied or moved, depending on the value of . + /// Extensions are used eg to copy MyDatabase.mdf to MyDatabase.mdf.temp. + /// + public void CopyDatabaseFiles(string databaseName, string filesPath, + string targetDatabaseName = null, string targetFilesPath = null, + string sourceExtension = null, string targetExtension = null, + bool overwrite = false, bool delete = false) + { + var nop = (targetFilesPath == null || targetFilesPath == filesPath) + && (targetDatabaseName == null || targetDatabaseName == databaseName) + && (sourceExtension == null && targetExtension == null || sourceExtension == targetExtension); + if (nop && delete == false) return; + + string logName, baseFilename, baseLogFilename, mdfFilename, ldfFilename; + GetDatabaseFiles(databaseName, filesPath, + out logName, out baseFilename, out baseLogFilename, out mdfFilename, out ldfFilename); + + if (sourceExtension != null) + { + mdfFilename += "." + sourceExtension; + ldfFilename += "." + sourceExtension; + } + + if (nop) + { + // delete + if (File.Exists(mdfFilename)) File.Delete(mdfFilename); + if (File.Exists(ldfFilename)) File.Delete(ldfFilename); + } + else + { + // copy or copy+delete ie move + string targetLogName, targetBaseFilename, targetLogFilename, targetMdfFilename, targetLdfFilename; + GetDatabaseFiles(targetDatabaseName ?? databaseName, targetFilesPath ?? filesPath, + out targetLogName, out targetBaseFilename, out targetLogFilename, out targetMdfFilename, out targetLdfFilename); + + if (targetExtension != null) + { + targetMdfFilename += "." + targetExtension; + targetLdfFilename += "." + targetExtension; + } + + if (delete) + { + if (overwrite && File.Exists(targetMdfFilename)) File.Delete(targetMdfFilename); + if (overwrite && File.Exists(targetLdfFilename)) File.Delete(targetLdfFilename); + File.Move(mdfFilename, targetMdfFilename); + File.Move(ldfFilename, targetLdfFilename); + } + else + { + File.Copy(mdfFilename, targetMdfFilename, overwrite); + File.Copy(ldfFilename, targetLdfFilename, overwrite); + } + } + } + + /// + /// Gets a value indicating whether database files exist. + /// + /// The name of the source database. + /// The directory containing source database files. + /// The database files extension. + /// A value indicating whether the database files exist. + /// + /// Extensions are used eg to copy MyDatabase.mdf to MyDatabase.mdf.temp. + /// + public bool DatabaseFilesExist(string databaseName, string filesPath, string extension = null) + { + string logName, baseFilename, baseLogFilename, mdfFilename, ldfFilename; + GetDatabaseFiles(databaseName, filesPath, + out logName, out baseFilename, out baseLogFilename, out mdfFilename, out ldfFilename); + + if (extension != null) + { + mdfFilename += "." + extension; + ldfFilename += "." + extension; + } + + return File.Exists(mdfFilename) && File.Exists(ldfFilename); + } + + /// + /// Gets the name of the database files. + /// + /// The name of the database. + /// The directory containing database files. + /// The name of the log. + /// The base filename (the MDF filename without the .mdf extension). + /// The base log filename (the LDF filename without the .ldf extension). + /// The MDF filename. + /// The LDF filename. + private static void GetDatabaseFiles(string databaseName, string filesPath, + out string logName, + out string baseFilename, out string baseLogFilename, + out string mdfFilename, out string ldfFilename) + { + logName = databaseName + "_log"; + baseFilename = Path.Combine(filesPath, databaseName); + baseLogFilename = Path.Combine(filesPath, logName); + mdfFilename = baseFilename + ".mdf"; + ldfFilename = baseFilename + "_log.ldf"; + } + + #endregion + + #region SqlLocalDB + + /// + /// Executes the SqlLocalDB command. + /// + /// The arguments. + /// The command standard output. + /// The command error output. + /// The process exit code. + /// + /// Execution is successful if the exit code is zero, and error is empty. + /// + private int ExecuteSqlLocalDb(string args, out string output, out string error) + { + var programFiles = Environment.GetEnvironmentVariable("ProgramFiles"); + if (programFiles == null) + { + output = string.Empty; + error = "SqlLocalDB.exe not found"; + return -1; + } + + var path = Path.Combine(programFiles, string.Format(@"Microsoft SQL Server\{0}0\Tools\Binn\SqlLocalDB.exe", _version)); + + var p = new Process + { + StartInfo = + { + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + FileName = path, + Arguments = args, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden + } + }; + p.Start(); + output = p.StandardOutput.ReadToEnd(); + error = p.StandardError.ReadToEnd(); + p.WaitForExit(); + + return p.ExitCode; + } + + /// + /// Returns a Unicode string with the delimiters added to make the input string a valid SQL Server delimited identifier. + /// + /// The name to quote. + /// A quote character. + /// + /// + /// This is a C# implementation of T-SQL QUOTEDNAME. + /// is optional, it can be '[' (default), ']', '\'' or '"'. + /// + private static string QuotedName(string name, char quote = '[') + { + switch (quote) + { + case '[': + case ']': + return "[" + name.Replace("]", "]]") + "]"; + case '\'': + return "'" + name.Replace("'", "''") + "'"; + case '"': + return "\"" + name.Replace("\"", "\"\"") + "\""; + default: + throw new NotSupportedException("Not a valid quote character."); + } + } + + #endregion + } +} diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index d384106293..fa907b0e05 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -438,7 +438,24 @@ namespace Umbraco.Core.Persistence.Repositories nodeDto.Path = parent.Path; nodeDto.Level = short.Parse(level.ToString(CultureInfo.InvariantCulture)); nodeDto.SortOrder = sortOrder; - var o = Database.IsNew(nodeDto) ? Convert.ToInt32(Database.Insert(nodeDto)) : Database.Update(nodeDto); + + // note: + // there used to be a check on Database.IsNew(nodeDto) here to either Insert or Update, + // but I cannot figure out what was the point, as the node should obviously be new if + // we reach that point - removed. + + // see if there's a reserved identifier for this unique id + var sql = new Sql("SELECT id FROM umbracoNode WHERE uniqueID=@0 AND nodeObjectType=@1", nodeDto.UniqueId, Constants.ObjectTypes.IdReservationGuid); + var id = Database.ExecuteScalar(sql); + if (id > 0) + { + nodeDto.NodeId = id; + Database.Update(nodeDto); + } + else + { + Database.Insert(nodeDto); + } //Update with new correct path nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); diff --git a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs index 48b9698474..34f69c60f8 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs @@ -86,7 +86,7 @@ namespace Umbraco.Core.Persistence.Repositories #endregion #region Overrides of PetaPocoRepositoryBase - + protected override Sql GetBaseQuery(BaseQueryType queryType) { var sql = new Sql(); @@ -157,7 +157,7 @@ namespace Umbraco.Core.Persistence.Repositories /// This is the underlying method that processes most queries for this repository /// /// - /// The full SQL to select all media data + /// The full SQL to select all media data /// /// /// The Id SQL to just return all media ids - used to process the properties for the media item @@ -168,7 +168,7 @@ namespace Umbraco.Core.Persistence.Repositories { // fetch returns a list so it's ok to iterate it in this method var dtos = Database.Fetch(sqlFull); - + //This is a tuple list identifying if the content item came from the cache or not var content = new List>(); var defs = new DocumentDefinitionCollection(); @@ -184,7 +184,7 @@ namespace Umbraco.Core.Persistence.Repositories if (withCache) { var cached = IsolatedCache.GetCacheItem(GetCacheIdKey(dto.NodeId)); - //only use this cached version if the dto returned is the same version - this is just a safety check, media doesn't + //only use this cached version if the dto returned is the same version - this is just a safety check, media doesn't //store different versions, but just in case someone corrupts some data we'll double check to be sure. if (cached != null && cached.Version == dto.VersionId) { @@ -307,7 +307,7 @@ namespace Umbraco.Core.Persistence.Repositories .From(SqlSyntax) .InnerJoin(SqlSyntax) .On(SqlSyntax, left => left.NodeId, right => right.NodeId); - + if (contentTypeIdsA.Length > 0) { xmlIdsQuery.InnerJoin(SqlSyntax) @@ -318,7 +318,7 @@ namespace Umbraco.Core.Persistence.Repositories } xmlIdsQuery.Where(dto => dto.NodeObjectType == mediaObjectType, SqlSyntax); - + var allXmlIds = Database.Fetch(xmlIdsQuery); var toRemove = allXmlIds.Except(allMediaIds).ToArray(); @@ -384,7 +384,24 @@ namespace Umbraco.Core.Persistence.Repositories nodeDto.Path = parent.Path; nodeDto.Level = short.Parse(level.ToString(CultureInfo.InvariantCulture)); nodeDto.SortOrder = sortOrder; - var o = Database.IsNew(nodeDto) ? Convert.ToInt32(Database.Insert(nodeDto)) : Database.Update(nodeDto); + + // note: + // there used to be a check on Database.IsNew(nodeDto) here to either Insert or Update, + // but I cannot figure out what was the point, as the node should obviously be new if + // we reach that point - removed. + + // see if there's a reserved identifier for this unique id + var sql = new Sql("SELECT id FROM umbracoNode WHERE uniqueID=@0 AND nodeObjectType=@1", nodeDto.UniqueId, Constants.ObjectTypes.IdReservationGuid); + var id = Database.ExecuteScalar(sql); + if (id > 0) + { + nodeDto.NodeId = id; + Database.Update(nodeDto); + } + else + { + Database.Insert(nodeDto); + } //Update with new correct path nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); @@ -666,7 +683,7 @@ namespace Umbraco.Core.Persistence.Repositories private IMedia CreateMediaFromDto(ContentVersionDto dto, Sql docSql) { var contentType = _mediaTypeRepository.Get(dto.ContentDto.ContentTypeId); - + var media = MediaFactory.BuildEntity(dto, contentType); var docDef = new DocumentDefinition(dto, contentType); @@ -686,7 +703,7 @@ namespace Umbraco.Core.Persistence.Repositories if (EnsureUniqueNaming == false) return nodeName; - var names = Database.Fetch("SELECT id, text AS name FROM umbracoNode WHERE nodeObjectType=@objectType AND parentId=@parentId", + var names = Database.Fetch("SELECT id, text AS name FROM umbracoNode WHERE nodeObjectType=@objectType AND parentId=@parentId", new { objectType = NodeObjectTypeId, parentId }); return SimilarNodeName.GetUniqueName(names, id, nodeName); diff --git a/src/Umbraco.Core/Security/BackOfficeUserManager.cs b/src/Umbraco.Core/Security/BackOfficeUserManager.cs index 5e04659b43..5a82df5b66 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserManager.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserManager.cs @@ -7,6 +7,8 @@ using System.Web.Security; using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.Owin; using Microsoft.Owin.Security.DataProtection; +using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Models.Identity; using Umbraco.Core.Services; @@ -24,14 +26,25 @@ namespace Umbraco.Core.Security { } + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Use the constructor specifying all dependencies instead")] public BackOfficeUserManager( IUserStore store, IdentityFactoryOptions options, MembershipProviderBase membershipProvider) + : this(store, options, membershipProvider, UmbracoConfig.For.UmbracoSettings().Content) + { + } + + public BackOfficeUserManager( + IUserStore store, + IdentityFactoryOptions options, + MembershipProviderBase membershipProvider, + IContentSection contentSectionConfig) : base(store) { - if (options == null) throw new ArgumentNullException("options");; - InitUserManager(this, membershipProvider, options); + if (options == null) throw new ArgumentNullException("options"); ; + InitUserManager(this, membershipProvider, contentSectionConfig, options); } #region Static Create methods @@ -46,7 +59,8 @@ namespace Umbraco.Core.Security { return Create(options, userService, ApplicationContext.Current.Services.EntityService, - externalLoginService, membershipProvider); + externalLoginService, membershipProvider, + UmbracoConfig.For.UmbracoSettings().Content); } /// @@ -57,20 +71,34 @@ namespace Umbraco.Core.Security /// /// /// + /// /// public static BackOfficeUserManager Create( IdentityFactoryOptions options, IUserService userService, IEntityService entityService, IExternalLoginService externalLoginService, - MembershipProviderBase membershipProvider) + MembershipProviderBase membershipProvider, + IContentSection contentSectionConfig) { if (options == null) throw new ArgumentNullException("options"); if (userService == null) throw new ArgumentNullException("userService"); if (externalLoginService == null) throw new ArgumentNullException("externalLoginService"); - var manager = new BackOfficeUserManager(new BackOfficeUserStore(userService, entityService, externalLoginService, membershipProvider)); - manager.InitUserManager(manager, membershipProvider, options); + var manager = new BackOfficeUserManager( + new BackOfficeUserStore(userService, entityService, externalLoginService, membershipProvider)); + manager.InitUserManager(manager, membershipProvider, contentSectionConfig, options); + return manager; + } + + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Use the overload specifying all dependencies instead")] + public static BackOfficeUserManager Create( + IdentityFactoryOptions options, + BackOfficeUserStore customUserStore, + MembershipProviderBase membershipProvider) + { + var manager = new BackOfficeUserManager(customUserStore, options, membershipProvider); return manager; } @@ -80,33 +108,46 @@ namespace Umbraco.Core.Security /// /// /// + /// /// public static BackOfficeUserManager Create( - IdentityFactoryOptions options, - BackOfficeUserStore customUserStore, - MembershipProviderBase membershipProvider) + IdentityFactoryOptions options, + BackOfficeUserStore customUserStore, + MembershipProviderBase membershipProvider, + IContentSection contentSectionConfig) { - var manager = new BackOfficeUserManager(customUserStore, options, membershipProvider); + var manager = new BackOfficeUserManager(customUserStore, options, membershipProvider, contentSectionConfig); return manager; } #endregion + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Use the overload specifying all dependencies instead")] + protected void InitUserManager( + BackOfficeUserManager manager, + MembershipProviderBase membershipProvider, + IdentityFactoryOptions options) + { + InitUserManager(manager, membershipProvider, UmbracoConfig.For.UmbracoSettings().Content, options); + } + /// /// Initializes the user manager with the correct options /// /// /// + /// /// /// protected void InitUserManager( BackOfficeUserManager manager, MembershipProviderBase membershipProvider, + IContentSection contentSectionConfig, IdentityFactoryOptions options) { //NOTE: This method is mostly here for backwards compat - base.InitUserManager(manager, membershipProvider, options.DataProtectionProvider); + base.InitUserManager(manager, membershipProvider, options.DataProtectionProvider, contentSectionConfig); } - } @@ -151,6 +192,16 @@ namespace Umbraco.Core.Security } #endregion + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Use the overload specifying all dependencies instead")] + protected void InitUserManager( + BackOfficeUserManager manager, + MembershipProviderBase membershipProvider, + IDataProtectionProvider dataProtectionProvider) + { + InitUserManager(manager, membershipProvider, dataProtectionProvider, UmbracoConfig.For.UmbracoSettings().Content); + } + /// /// Initializes the user manager with the correct options /// @@ -159,11 +210,13 @@ namespace Umbraco.Core.Security /// The for the users called UsersMembershipProvider /// /// + /// /// protected void InitUserManager( BackOfficeUserManager manager, MembershipProviderBase membershipProvider, - IDataProtectionProvider dataProtectionProvider) + IDataProtectionProvider dataProtectionProvider, + IContentSection contentSectionConfig) { // Configure validation logic for usernames manager.UserValidator = new BackOfficeUserValidator(manager) @@ -193,7 +246,9 @@ namespace Umbraco.Core.Security //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(); + manager.EmailService = new EmailService( + contentSectionConfig.NotificationEmailAddress, + new EmailSender()); //NOTE: Not implementing these, if people need custom 2 factor auth, they'll need to implement their own UserStore to suport it diff --git a/src/Umbraco.Core/Security/EmailService.cs b/src/Umbraco.Core/Security/EmailService.cs index f8f9af10ae..51d1b82207 100644 --- a/src/Umbraco.Core/Security/EmailService.cs +++ b/src/Umbraco.Core/Security/EmailService.cs @@ -1,16 +1,37 @@ -using System.Net.Mail; +using System; +using System.ComponentModel; +using System.Net.Mail; using System.Threading.Tasks; using Microsoft.AspNet.Identity; using Umbraco.Core.Configuration; namespace Umbraco.Core.Security { + /// + /// The implementation for Umbraco + /// public class EmailService : IIdentityMessageService { + private readonly string _notificationEmailAddress; + private readonly IEmailSender _defaultEmailSender; + + public EmailService(string notificationEmailAddress, IEmailSender defaultEmailSender) + { + _notificationEmailAddress = notificationEmailAddress; + _defaultEmailSender = defaultEmailSender; + } + + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Use the constructor specifying all dependencies")] + public EmailService() + : this(UmbracoConfig.For.UmbracoSettings().Content.NotificationEmailAddress, new EmailSender()) + { + } + public async Task SendAsync(IdentityMessage message) { var mailMessage = new MailMessage( - UmbracoConfig.For.UmbracoSettings().Content.NotificationEmailAddress, + _notificationEmailAddress, message.Destination, message.Subject, message.Body) @@ -21,16 +42,15 @@ namespace Umbraco.Core.Security try { - using (var client = new SmtpClient()) + //check if it's a custom message and if so use it's own defined mail sender + var umbMsg = message as UmbracoEmailMessage; + if (umbMsg != null) { - if (client.DeliveryMethod == SmtpDeliveryMethod.Network) - { - await client.SendMailAsync(mailMessage); - } - else - { - client.Send(mailMessage); - } + await umbMsg.MailSender.SendAsync(mailMessage); + } + else + { + await _defaultEmailSender.SendAsync(mailMessage); } } finally diff --git a/src/Umbraco.Core/Security/UmbracoEmailMessage.cs b/src/Umbraco.Core/Security/UmbracoEmailMessage.cs new file mode 100644 index 0000000000..9ef6205ebf --- /dev/null +++ b/src/Umbraco.Core/Security/UmbracoEmailMessage.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNet.Identity; + +namespace Umbraco.Core.Security +{ + /// + /// A custom implementation for IdentityMessage that allows the customization of how an email is sent + /// + internal class UmbracoEmailMessage : IdentityMessage + { + public IEmailSender MailSender { get; private set; } + + public UmbracoEmailMessage(IEmailSender mailSender) + { + MailSender = mailSender; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index b243cc5363..b512e7e337 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -684,7 +684,17 @@ namespace Umbraco.Core.Services // get query - if the id is System Root, then just get all var query = Query.Builder; if (id != Constants.System.Root) - query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); + { + var entityRepository = RepositoryFactory.CreateEntityRepository(uow); + var contentPath = entityRepository.GetAllPaths(Constants.ObjectTypes.DocumentGuid, id).ToArray(); + if (contentPath.Length == 0) + { + totalChildren = 0; + return Enumerable.Empty(); + } + query.Where(x => x.Path.SqlStartsWith(string.Format("{0},", contentPath[0]), TextColumnType.NVarchar)); + } + // get filter IQuery filterQuery = null; @@ -719,7 +729,16 @@ namespace Umbraco.Core.Services // get query - if the id is System Root, then just get all var query = Query.Builder; if (id != Constants.System.Root) - query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); + { + var entityRepository = RepositoryFactory.CreateEntityRepository(uow); + var contentPath = entityRepository.GetAllPaths(Constants.ObjectTypes.DocumentGuid, id).ToArray(); + if (contentPath.Length == 0) + { + totalChildren = 0; + return Enumerable.Empty(); + } + query.Where(x => x.Path.SqlStartsWith(string.Format("{0},", contentPath[0]), TextColumnType.NVarchar)); + } return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filter); } @@ -942,15 +961,30 @@ namespace Umbraco.Core.Services /// True if the Content can be published, otherwise False public bool IsPublishable(IContent content) { - //If the passed in content has yet to be saved we "fallback" to checking the Parent - //because if the Parent is publishable then the current content can be Saved and Published - if (content.HasIdentity == false) - { - var parent = GetById(content.ParentId); - return IsPublishable(parent, true); - } + // get ids from path + // skip the first one that has to be -1 - and we don't care + // skip the last one that has to be "this" - and it's ok to stop at the parent + var ids = content.Path.Split(',').Skip(1).SkipLast().Select(int.Parse).ToArray(); + if (ids.Length == 0) + return false; - return IsPublishable(content, false); + // if the first one is recycle bin, fail fast + if (ids[0] == Constants.System.RecycleBinContent) + return false; + + // fixme - move to repository? + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var sql = new Sql(@" + SELECT id + FROM umbracoNode + JOIN cmsDocument ON umbracoNode.id=cmsDocument.nodeId AND cmsDocument.published=@0 + WHERE umbracoNode.trashed=@1 AND umbracoNode.id IN (@2)", + true, false, ids); + Console.WriteLine(sql.SQL); + var x = uow.Database.Fetch(sql); + return ids.Length == x.Count; + } } /// @@ -1101,7 +1135,7 @@ namespace Umbraco.Core.Services descendant.WriterId = userId; descendant.ChangeTrashedState(true, descendant.ParentId); repository.AddOrUpdate(descendant); - + moveInfo.Add(new MoveEventInfo(descendant, descendant.Path, descendant.ParentId)); } @@ -1667,7 +1701,7 @@ namespace Umbraco.Core.Services { using (new WriteLock(Locker)) { - var nodeObjectType = new Guid(Constants.ObjectTypes.Document); + var nodeObjectType = Constants.ObjectTypes.DocumentGuid; using (var uow = UowProvider.GetUnitOfWork()) { @@ -2470,40 +2504,6 @@ namespace Umbraco.Core.Services } } - /// - /// Checks if the passed in can be published based on the anscestors publish state. - /// - /// - /// Check current is only used when falling back to checking the Parent of non-saved content, as - /// non-saved content doesn't have a valid path yet. - /// - /// to check if anscestors are published - /// Boolean indicating whether the passed in content should also be checked for published versions - /// True if the Content can be published, otherwise False - private bool IsPublishable(IContent content, bool checkCurrent) - { - var ids = content.Path.Split(',').Select(int.Parse).ToList(); - foreach (var id in ids) - { - //If Id equals that of the recycle bin we return false because nothing in the bin can be published - if (id == Constants.System.RecycleBinContent) - return false; - - //We don't check the System Root, so just continue - if (id == Constants.System.Root) continue; - - //If the current id equals that of the passed in content and if current shouldn't be checked we skip it. - if (checkCurrent == false && id == content.Id) continue; - - //Check if the content for the current id is published - escape the loop if we encounter content that isn't published - var hasPublishedVersion = HasPublishedVersion(id); - if (hasPublishedVersion == false) - return false; - } - - return true; - } - private PublishStatusType CheckAndLogIsPublishable(IContent content) { //Check if parent is published (although not if its a root node) - if parent isn't published this Content cannot be published diff --git a/src/Umbraco.Core/Services/EntityService.cs b/src/Umbraco.Core/Services/EntityService.cs index 6e4b5ef17c..b660927df3 100644 --- a/src/Umbraco.Core/Services/EntityService.cs +++ b/src/Umbraco.Core/Services/EntityService.cs @@ -348,11 +348,22 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.GetUnitOfWork(readOnly:true)) { var repository = RepositoryFactory.CreateEntityRepository(uow); - + var query = Query.Builder; //if the id is System Root, then just get all if (id != Constants.System.Root) - query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); + { + //lookup the path so we can use it in the prefix query below + var itemPaths = repository.GetAllPaths(objectTypeId, id).ToArray(); + if (itemPaths.Length == 0) + { + totalRecords = 0; + return Enumerable.Empty(); + } + var itemPath = itemPaths[0].Path; + + query.Where(x => x.Path.SqlStartsWith(string.Format("{0},", itemPath), TextColumnType.NVarchar)); + } IQuery filterQuery = null; if (filter.IsNullOrWhiteSpace() == false) @@ -381,15 +392,30 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { var repository = RepositoryFactory.CreateEntityRepository(uow); - + var query = Query.Builder; if (idsA.All(x => x != Constants.System.Root)) { + //lookup the paths so we can use it in the prefix query below + var itemPaths = repository.GetAllPaths(objectTypeId, idsA).ToArray(); + if (itemPaths.Length == 0) + { + totalRecords = 0; + return Enumerable.Empty(); + } + var clauses = new List>>(); foreach (var id in idsA) - { - var qid = id; - clauses.Add(x => x.Path.SqlContains(string.Format(",{0},", qid), TextColumnType.NVarchar) || x.Path.SqlEndsWith(string.Format(",{0}", qid), TextColumnType.NVarchar)); + { + //if the id is root then don't add any clauses + if (id != Constants.System.Root) + { + var itemPath = itemPaths.FirstOrDefault(x => x.Id == id); + if (itemPath == null) continue; + var path = itemPath.Path; + var qid = id; + clauses.Add(x => x.Path.SqlStartsWith(string.Format("{0},", path), TextColumnType.NVarchar) || x.Path.SqlEndsWith(string.Format(",{0}", qid), TextColumnType.NVarchar)); + } } query.WhereAny(clauses); } @@ -442,7 +468,7 @@ namespace Umbraco.Core.Services return contents; } } - + /// /// Gets a collection of descendents by the parents Id /// @@ -601,13 +627,13 @@ namespace Umbraco.Core.Services return repository.GetAllPaths(objectTypeId, keys); } } - + /// - /// Gets a collection of + /// Gets a collection of /// /// Guid id of the UmbracoObjectType /// - /// An enumerable list of objects + /// An enumerable list of objects public virtual IEnumerable GetAll(Guid objectTypeId, params int[] ids) { var umbracoObjectType = UmbracoObjectTypesExtensions.GetUmbracoObjectType(objectTypeId); @@ -725,5 +751,34 @@ namespace Umbraco.Core.Services return exists; } } + + /// + public int ReserveId(Guid key) + { + NodeDto node; + using (var scope = UowProvider.ScopeProvider.CreateScope()) + { + var sql = new Sql("SELECT * FROM umbracoNode WHERE uniqueID=@0 AND nodeObjectType=@1", key, Constants.ObjectTypes.IdReservationGuid); + node = scope.Database.SingleOrDefault(sql); + if (node != null) throw new InvalidOperationException("An identifier has already been reserved for this Udi."); + node = new NodeDto + { + UniqueId = key, + Text = "RESERVED.ID", + NodeObjectType = Constants.ObjectTypes.IdReservationGuid, + + CreateDate = DateTime.Now, + UserId = 0, + ParentId = -1, + Level = 1, + Path = "-1", + SortOrder = 0, + Trashed = false + }; + scope.Database.Insert(node); + scope.Complete(); + } + return node.NodeId; + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/IEntityService.cs b/src/Umbraco.Core/Services/IEntityService.cs index 3136a49bf3..d8a60a9cae 100644 --- a/src/Umbraco.Core/Services/IEntityService.cs +++ b/src/Umbraco.Core/Services/IEntityService.cs @@ -170,7 +170,7 @@ namespace Umbraco.Core.Services /// /// /// - /// + /// /// IEnumerable GetPagedDescendants(int id, UmbracoObjectTypes umbracoObjectType, long pageIndex, int pageSize, out long totalRecords, string orderBy = "path", Direction orderDirection = Direction.Ascending, string filter = ""); @@ -286,5 +286,13 @@ namespace Umbraco.Core.Services /// /// Type of the entity Type GetEntityType(UmbracoObjectTypes umbracoObjectType); + + /// + /// Reserves an identifier for a key. + /// + /// They key. + /// The identifier. + /// When a new content or a media is saved with the key, it will have the reserved identifier. + int ReserveId(Guid key); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/IdkMap.cs b/src/Umbraco.Core/Services/IdkMap.cs index 015bb104dc..411316fca2 100644 --- a/src/Umbraco.Core/Services/IdkMap.cs +++ b/src/Umbraco.Core/Services/IdkMap.cs @@ -46,6 +46,10 @@ namespace Umbraco.Core.Services if (val == null) return Attempt.Fail(); + // cache reservations, when something is saved this cache is cleared anyways + //if (umbracoObjectType == UmbracoObjectTypes.IdReservation) + // Attempt.Succeed(val.Value); + try { _locker.EnterWriteLock(); @@ -95,6 +99,10 @@ namespace Umbraco.Core.Services if (val == null) return Attempt.Fail(); + // cache reservations, when something is saved this cache is cleared anyways + //if (umbracoObjectType == UmbracoObjectTypes.IdReservation) + // Attempt.Succeed(val.Value); + try { _locker.EnterWriteLock(); diff --git a/src/Umbraco.Core/Services/MediaService.cs b/src/Umbraco.Core/Services/MediaService.cs index 3b74dcd487..1ec429af0c 100644 --- a/src/Umbraco.Core/Services/MediaService.cs +++ b/src/Umbraco.Core/Services/MediaService.cs @@ -604,7 +604,14 @@ namespace Umbraco.Core.Services //if the id is System Root, then just get all if (id != Constants.System.Root) { - query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); + var entityRepository = RepositoryFactory.CreateEntityRepository(uow); + var mediaPath = entityRepository.GetAllPaths(Constants.ObjectTypes.MediaGuid, id).ToArray(); + if (mediaPath.Length == 0) + { + totalChildren = 0; + return Enumerable.Empty(); + } + query.Where(x => x.Path.SqlStartsWith(string.Format("{0},", mediaPath[0]), TextColumnType.NVarchar)); } IQuery filterQuery = null; if (filter.IsNullOrWhiteSpace() == false) diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index 685c4f22ed..89232f815e 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -712,12 +712,11 @@ namespace Umbraco.Core.Services /// Id of the User to retrieve /// public IProfile GetProfileById(int id) - { - using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) - { - var repository = RepositoryFactory.CreateUserRepository(uow); - return repository.GetProfile(id); - } + { + //This is called a TON. Go get the full user from cache which should already be IProfile + var fullUser = GetUserById(id); + var asProfile = fullUser as IProfile; + return asProfile ?? new UserProfile(fullUser.Id, fullUser.Name); } /// diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 7817b24ff1..6f3e78a78e 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -348,14 +348,17 @@ + + + @@ -527,6 +530,7 @@ + @@ -688,6 +692,7 @@ + diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index c9daeb0022..0a37b87d03 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -1099,6 +1099,21 @@ namespace Umbraco.Tests.Services Assert.That(content.Published, Is.True); } + [Test] + public void IsPublishable() + { + // Arrange + var contentService = ServiceContext.ContentService; + var parent = contentService.CreateContent("parent", -1, "umbTextpage"); + contentService.SaveAndPublishWithStatus(parent); + var content = contentService.CreateContent("child", parent, "umbTextpage"); + contentService.Save(content); + + Assert.IsTrue(contentService.IsPublishable(content)); + contentService.UnPublish(parent); + Assert.IsFalse(contentService.IsPublishable(content)); + } + [Test] public void Can_Publish_Content_WithEvents() { diff --git a/src/Umbraco.Web.UI.Client/.eslintrc b/src/Umbraco.Web.UI.Client/.eslintrc index 811fabdabf..205d8dcf50 100644 --- a/src/Umbraco.Web.UI.Client/.eslintrc +++ b/src/Umbraco.Web.UI.Client/.eslintrc @@ -39,7 +39,7 @@ "globals": { "angular": false, "_": false, - "$", false, + "$": false, "tinymce": false, "tinyMCE": false, "FileReader": false, @@ -47,7 +47,7 @@ "window": false, "LazyLoad": false, "ActiveXObject": false, - "Bloodhound", false + "Bloodhound": false } } \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/README.md b/src/Umbraco.Web.UI.Client/README.md deleted file mode 100644 index 6001d65271..0000000000 --- a/src/Umbraco.Web.UI.Client/README.md +++ /dev/null @@ -1,97 +0,0 @@ -#Belle - -Umbraco 7 UI, codename "Belle" Built on AngularJS, bower, Lazyload.js and Twitter Bootstrap - - -##Introduction -Slides from the initial demonstration of Belle done at the Umbraco DK Fest can be found here: - -http://rawgithub.com/umbraco/Umbraco.Web.Ui.Client/build/master/Presentation/index.html - - -##Running the site with mocked data - -This won't require any database or setup, as everything is running through node. All you have to do is install -node and grunt on either windows or OSX and the entire setup is ready for you. - - -###Install node.js -We need node to run tests and automated less compiling and other automated tasks. go to http://nodejs.org. Node.js is a powerfull javascript engine, which allows us to run all our tests and tasks written in javascript locally. - -*note:* On windows you might need to restart explorer.exe to register node. - - -###Install dependencies -Next we need to install all the required packages. This is done with the package tool, included with node.js, open /src/Umbraco.Web.UI.Client in cmd.exe or osx terminal and run the command: - - npm install - -this will fetch all needed packages to your local machine. - - -###Install grunt globally -Grunt is a task runner for node.js, and we use it for all automated tasks in the build process. For convenience we need to install it globally on your machine, so it can be used directly in cmd.exe or the terminal. - -So run the command: - - npm install grunt-cli -g - -*note:* On windows you might need to restart explorer.exe to register the grunt cmd. - -*note:* On OSX you might need to run: - - sudo npm install grunt-cli -g - -Now that you have node and grunt installed, you can open `/src/Umbraco.Web.UI.Client` in either `cmd.exe` or terminal and run: - - grunt dev - -This will build the site, merge less files, run tests and create the /Build folder, and finally open the site in your -browser. - - -##Limitations -The current prototype simply uses in-memory storage, so no database dependencies. It is aimed at showing UI, not a complete functional client-server setup. - - -##Project Structure - -All project files are located in /src/Umbraco.Web.UI.Client which only contains client-side files, everything -related to asp.net are in /src/Umbraco.Web.UI - -after building Belle files are located in /build/belle, with all files following AngularJs -conventions: - -###Folders -- */Umbraco.Web.Ui.Client/build/lib:* Dependencies -- */Umbraco.Web.Ui.Client/build/js:* Application javascript files -- */Umbraco.Web.Ui.Client/build/views/common/:* Main application views -- */Umbraco.Web.Ui.Client/build/views/[sectioname]/pagename Editors html -- */Umbraco.Web.Ui.Client/build/views/propertyeditors:* Property Editors html - - -###Files -- */Umbraco.Web.Ui.Client/build/js/app.js:* Main umbraco application / modules -- */Umbraco.Web.Ui.Client/build/js/loader.js:* lazyload configuration for dependencies -- */Umbraco.Web.Ui.Client/build/js/routes.js:* Application routes -- */Umbraco.Web.Ui.Client/build/js/umbraco.controllers.js:* Application controllers -- */Umbraco.Web.Ui.Client/build/js/umbraco.services.js:* Application services -- */Umbraco.Web.Ui.Client/build/js/umbraco.filters.js:* Application filters -- */Umbraco.Web.Ui.Client/build/js/umbraco.directives.js:* Application directives -- */Umbraco.Web.Ui.Client/build/js/umbraco.resources.js:* Application resources, like content, media, users, members etc -- */Umbraco.Web.Ui.Client/build/js/umbraco.mocks.js:* Fake Application resources, for running the app without a server - -##Getting started -The current app is built, following conventions from angularJs and bootstrap. To get started with the applicaton you will need to atleast know the basics of these frameworks - -###AngularJS -- Excellent introduction videos on http://www.egghead.io/ -- Official guide at: http://docs.angularjs.org/guide/ - -###Require.js -- Introduction: http://javascriptplayground.com/blog/2012/07/requirejs-amd-tutorial-introduction -- Require.js website: http://requirejs.org/ - - - - diff --git a/src/Umbraco.Web.UI.Client/bower.json b/src/Umbraco.Web.UI.Client/bower.json index 0ae1e2f214..53ea30f9d5 100644 --- a/src/Umbraco.Web.UI.Client/bower.json +++ b/src/Umbraco.Web.UI.Client/bower.json @@ -30,6 +30,59 @@ "angular-local-storage": "~0.2.3", "moment": "~2.10.3", "ace-builds": "^1.2.3", + "font-awesome": "~4.2", "clipboard": "1.7.1" + }, + + "install": { + + "path": "lib-bower", + + "ignore": [ + "font-awesome", + "angular", + "bootstrap", + "codemirror" + ], + + "sources": { + "moment": "bower_components/moment/min/moment-with-locales.js", + + "underscore": [ + "bower_components/underscore/underscore-min.js", + "bower_components/underscore/underscore-min.map" + ], + + "jquery": [ + "bower_components/jquery/dist/jquery.min.js", + "bower_components/jquery/dist/jquery.min.map" + ], + + "angular-dynamic-locale": [ + "bower_components/angular-dynamic-locale/tmhDynamicLocale.min.js", + "bower_components/angular-dynamic-locale/tmhDynamicLocale.min.js.map" + ], + + "angular-local-storage": [ + "bower_components/angular-local-storage/dist/angular-local-storage.min.js", + "bower_components/angular-local-storage/dist/angular-local-storage.min.js.map" + ], + + "tinymce": [ + "bower_components/tinymce/tinymce.min.js" + ], + + "typeahead.js": "bower_components/typeahead.js/dist/typeahead.bundle.min.js", + + "rgrove-lazyload":"bower_components/rgrove-lazyload/lazyload.js", + + "ng-file-upload":"bower_components/ng-file-upload/ng-file-upload.min.js", + + "jquery-ui":"bower_components/jquery-ui/jquery-ui.min.js", + + "jquery-migrate":"bower_components/jquery-migrate/jquery-migrate.min.js", + + "clipboard": "bower_components/clipboard/dist/clipboard.min.js" + } } } diff --git a/src/Umbraco.Web.UI.Client/gruntFile.js b/src/Umbraco.Web.UI.Client/gruntFile.js deleted file mode 100644 index 5170fddcba..0000000000 --- a/src/Umbraco.Web.UI.Client/gruntFile.js +++ /dev/null @@ -1,601 +0,0 @@ -module.exports = function (grunt) { - - - - // Default task. - grunt.registerTask('default', ['jshint:dev', 'build', 'karma:unit']); - grunt.registerTask('dev', ['jshint:dev', 'build-dev', 'webserver', 'open:dev', 'watch']); - grunt.registerTask('docserve', ['docs:api', 'connect:docserver', 'open:docs', 'watch:docs']); - grunt.registerTask('vs', ['jshint:dev', 'build-dev', 'watch']); - - //TODO: Too much watching, this brings windows to it's knees when in dev mode - //run by the watch task - grunt.registerTask('watch-js', ['jshint:dev', 'concat', 'copy:app', 'copy:mocks', 'copy:canvasdesigner', 'copy:vs', 'karma:unit']); - grunt.registerTask('watch-less', ['recess:build', 'recess:installer', 'recess:nonodes', 'recess:canvasdesigner', 'postcss', 'copy:canvasdesigner', 'copy:assets', 'copy:vs']); - grunt.registerTask('watch-html', ['copy:views', 'copy:vs']); - grunt.registerTask('watch-installer', ['concat:install', 'concat:installJs', 'copy:installer', 'copy:vs']); - grunt.registerTask('watch-canvasdesigner', ['copy:canvasdesigner', 'concat:canvasdesignerJs', 'copy:vs']); - grunt.registerTask('watch-test', ['jshint:dev', 'karma:unit']); - - //triggered from grunt - grunt.registerTask('build', ['concat', 'recess:build', 'recess:installer', 'recess:nonodes', 'recess:canvasdesigner', 'postcss', 'bower-install-simple', 'bower', 'copy', 'clean:post']); - - //triggered from grunt dev vs or grunt vs - grunt.registerTask('build-dev', ['clean:pre', 'concat', 'recess:build', 'recess:installer', 'recess:nonodes', 'postcss', 'bower-install-simple', 'bower', 'copy']); - - //utillity tasks - grunt.registerTask('docs', ['ngdocs']); - grunt.registerTask('webserver', ['connect:devserver']); - - - // Print a timestamp (useful for when watching) - grunt.registerTask('timestamp', function () { - grunt.log.subhead(Date()); - }); - - // Project configuration. - grunt.initConfig({ - buildVersion: grunt.option('buildversion') || '7', - connect: { - devserver: { - options: { - port: 9990, - hostname: '0.0.0.0', - base: './build', - middleware: function(connect, options) { - return [ - //uncomment to enable CSP - // util.csp(), - //util.rewrite(), - connect.favicon('images/favicon.ico'), - connect.static(options.base), - connect.directory(options.base) - ]; - } - } - }, - testserver: {}, - docserver: { - options: { - port: 8880, - hostname: '0.0.0.0', - base: './docs/api', - middleware: function(connect, options) { - return [ - //uncomment to enable CSP - // util.csp(), - //util.rewrite(), - connect.static(options.base), - connect.directory(options.base) - ]; - } - } - }, - }, - - open: { - dev: { - path: 'http://localhost:9990/belle/' - }, - docs: { - path: 'http://localhost:8880/index.html' - } - }, - - distdir: 'build/belle', - vsdir: '../Umbraco.Web.UI/umbraco', - pkg: grunt.file.readJSON('package.json'), - banner: - '/*! <%= pkg.title || pkg.name %>\n' + - '<%= pkg.homepage ? " * " + pkg.homepage + "\\n" : "" %>' + - ' * Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author %>;\n' + - ' * Licensed <%= _.pluck(pkg.licenses, "type").join(", ") %>\n */\n', - src: { - js: ['src/**/*.js', 'src/*.js'], - - common: ['src/common/**/*.js'], - controllers: ['src/**/*.controller.js'], - - specs: ['test/**/*.spec.js'], - scenarios: ['test/**/*.scenario.js'], - samples: ['sample files/*.js'], - html: ['src/index.html', 'src/install.html'], - - everything: ['src/**/*.*', 'test/**/*.*', 'docs/**/*.*'], - - tpl: { - app: ['src/views/**/*.html'], - common: ['src/common/**/*.tpl.html'] - }, - less: ['src/less/belle.less'], // recess:build doesn't accept ** in its file patterns - prod: ['<%= distdir %>/js/*.js'] - }, - - clean: { - pre: ['<%= distdir %>/*'], - post: ['<%= distdir %>/js/*.dev.js'] - }, - - copy: { - assets: { - files: [{ dest: '<%= distdir %>/assets', src: '**', expand: true, cwd: 'src/assets/' }] - }, - - config: { - files: [{ dest: '<%= distdir %>/../config', src: '**', expand: true, cwd: 'src/config/' }] - }, - - installer: { - files: [{ dest: '<%= distdir %>/views/install', src: '**/*.html', expand: true, cwd: 'src/installer/steps' }] - }, - - canvasdesigner: { - files: [ - { dest: '<%= distdir %>/preview', src: '**/*.html', expand: true, cwd: 'src/canvasdesigner' }, - { dest: '<%= distdir %>/preview/editors', src: '**/*.html', expand: true, cwd: 'src/canvasdesigner/editors' }, - { dest: '<%= distdir %>/assets/less', src: '**/*.less', expand: true, cwd: 'src/canvasdesigner/editors' }, - { dest: '<%= distdir %>/js', src: 'canvasdesigner.config.js', expand: true, cwd: 'src/canvasdesigner/config' }, - { dest: '<%= distdir %>/js', src: 'canvasdesigner.palettes.js', expand: true, cwd: 'src/canvasdesigner/config' }, - { dest: '<%= distdir %>/js', src: 'canvasdesigner.front.js', expand: true, cwd: 'src/canvasdesigner' } - ] - }, - - vendor: { - files: [{ dest: '<%= distdir %>/lib', src: '**', expand: true, cwd: 'lib/' }] - }, - - views: { - files: [{ dest: '<%= distdir %>/views', src: ['**/*.*', '!**/*.controller.js'], expand: true, cwd: 'src/views' }] - }, - - app: { - files: [ - { dest: '<%= distdir %>/js', src: '*.js', expand: true, cwd: 'src/' } - ] - }, - - mocks: { - files: [{ dest: '<%= distdir %>/js', src: '*.js', expand: true, cwd: 'src/common/mocks/' }] - }, - - vs: { - files: [ - //everything except the index.html root file! - //then we need to figure out how to not copy all the test stuff either!? - { dest: '<%= vsdir %>/assets', src: '**', expand: true, cwd: '<%= distdir %>/assets' }, - { dest: '<%= vsdir %>/js', src: '**', expand: true, cwd: '<%= distdir %>/js' }, - { dest: '<%= vsdir %>/views', src: '**', expand: true, cwd: '<%= distdir %>/views' }, - { dest: '<%= vsdir %>/preview', src: '**', expand: true, cwd: '<%= distdir %>/preview' }, - { dest: '<%= vsdir %>/lib', src: '**', expand: true, cwd: '<%= distdir %>/lib' } - ] - } - }, - - karma: { - unit: { configFile: 'test/config/karma.conf.js', keepalive: true }, - e2e: { configFile: 'test/config/e2e.js', keepalive: true }, - watch: { configFile: 'test/config/unit.js', singleRun: false, autoWatch: true, keepalive: true } - }, - - concat: { - index: { - src: ['src/index.html'], - dest: '<%= distdir %>/index.html', - options: { - process: true - } - }, - install: { - src: ['src/installer/installer.html'], - dest: '<%= distdir %>/installer.html', - options: { - process: true - } - }, - - installJs: { - src: ['src/installer/**/*.js'], - dest: '<%= distdir %>/js/umbraco.installer.js', - options: { - banner: "<%= banner %>\n(function() { \n\n", - footer: "\n\n})();" - } - }, - - canvasdesignerJs: { - src: ['src/canvasdesigner/canvasdesigner.global.js', 'src/canvasdesigner/canvasdesigner.controller.js', 'src/canvasdesigner/editors/*.js', 'src/canvasdesigner/lib/*.js'], - dest: '<%= distdir %>/js/canvasdesigner.panel.js' - }, - - controllers: { - src: ['src/controllers/**/*.controller.js', 'src/views/**/*.controller.js'], - dest: '<%= distdir %>/js/umbraco.controllers.js', - options: { - banner: "<%= banner %>\n(function() { \n\n", - footer: "\n\n})();" - } - }, - - services: { - src: ['src/common/services/*.js'], - dest: '<%= distdir %>/js/umbraco.services.js', - options: { - banner: "<%= banner %>\n(function() { \n\n", - footer: "\n\n})();" - } - }, - - security: { - src: ['src/common/security/*.js'], - dest: '<%= distdir %>/js/umbraco.security.js', - options: { - banner: "<%= banner %>\n(function() { \n\n", - footer: "\n\n})();" - } - }, - - resources: { - src: ['src/common/resources/*.js'], - dest: '<%= distdir %>/js/umbraco.resources.js', - options: { - banner: "<%= banner %>\n(function() { \n\n", - footer: "\n\n})();" - } - }, - - testing: { - src: ['src/common/mocks/*/*.js'], - dest: '<%= distdir %>/js/umbraco.testing.js', - options: { - banner: "<%= banner %>\n(function() { \n\n", - footer: "\n\n})();" - } - }, - - directives: { - src: ['src/common/directives/**/*.js'], - dest: '<%= distdir %>/js/umbraco.directives.js', - options: { - banner: "<%= banner %>\n(function() { \n\n", - footer: "\n\n})();" - } - }, - - filters: { - src: ['src/common/filters/*.js'], - dest: '<%= distdir %>/js/umbraco.filters.js', - options: { - banner: "<%= banner %>\n(function() { \n\n", - footer: "\n\n})();" - } - } - }, - - uglify: { - options: { - mangle: true - }, - combine: { - files: { - '<%= distdir %>/js/umbraco.min.js': ['<%= distdir %>/js/umbraco.*.js'] - } - } - }, - - recess: { - build: { - files: { - '<%= distdir %>/assets/css/<%= pkg.name %>.css': - ['<%= src.less %>'] - }, - options: { - compile: true, - compress: true - } - }, - nonodes: { - files: { - '<%= distdir %>/assets/css/nonodes.style.min.css': - ['src/less/pages/nonodes.less'] - }, - options: { - compile: true, - compress: true - } - }, - installer: { - files: { - '<%= distdir %>/assets/css/installer.css': - ['src/less/installer.less'] - }, - options: { - compile: true, - compress: true - } - }, - canvasdesigner: { - files: { - '<%= distdir %>/assets/css/canvasdesigner.css': - ['src/less/canvas-designer.less', 'src/less/helveticons.less'] - }, - options: { - compile: true, - compress: true - } - } - }, - - postcss: { - options: { - processors: [ - // add vendor prefixes - require('autoprefixer-core')({ - browsers: 'last 2 versions' - }) - ] - }, - dist: { - src: '<%= distdir %>/assets/css/<%= pkg.name %>.css' - } - }, - - ngTemplateCache: { - views: { - files: { - '<%= distdir %>/js/umbraco.views.js': 'src/views/**/*.html' - }, - options: { - trim: 'src/', - module: 'umbraco.views' - } - } - }, - - watch: { - docs: { - files: ['docs/src/**/*.md'], - tasks: ['watch-docs', 'timestamp'] - }, - css: { - files: 'src/**/*.less', - tasks: ['watch-less', 'timestamp'], - options: { - livereload: true, - }, - }, - js: { - files: ['src/**/*.js', 'src/*.js'], - tasks: ['watch-js', 'timestamp'], - }, - test: { - files: ['test/**/*.js'], - tasks: ['watch-test', 'timestamp'], - }, - installer: { - files: ['src/installer/**/*.*'], - tasks: ['watch-installer', 'timestamp'], - }, - canvasdesigner: { - files: ['src/canvasdesigner/**/*.*'], - tasks: ['watch-canvasdesigner', 'timestamp'], - }, - html: { - files: ['src/views/**/*.html', 'src/*.html'], - tasks: ['watch-html', 'timestamp'] - }, - options: { - interval: 500 - } - }, - - - ngdocs: { - options: { - dest: 'docs/api', - startPage: '/api', - title: "Umbraco Backoffice UI API Documentation", - html5Mode: false, - styles: [ - 'docs/umb-docs.css' - ], - image: "https://our.umbraco.org/assets/images/logo.svg" - }, - api: { - src: ['src/common/**/*.js', 'docs/src/api/**/*.ngdoc'], - title: 'API Documentation' - }, - tutorials: { - src: [], - title: '' - } - }, - - eslint:{ - src: ['<%= src.common %>','<%= src.controllers %>'], - options: {quiet: true} - }, - - jshint: { - dev: { - files: { - src: ['<%= src.common %>'] - }, - options: { - curly: true, - eqeqeq: true, - immed: true, - latedef: "nofunc", - newcap: true, - noarg: true, - sub: true, - boss: true, - //NOTE: This is required so it doesn't barf on reserved words like delete when doing $http.delete - es5: true, - eqnull: true, - //NOTE: we need to use eval sometimes so ignore it - evil: true, - //NOTE: we need to check for strings such as "javascript:" so don't throw errors regarding those - scripturl: true, - //NOTE: we ignore tabs vs spaces because enforcing that causes lots of errors depending on the text editor being used - smarttabs: true, - globals: {} - } - }, - build: { - files: { - src: ['<%= src.prod %>'] - }, - options: { - curly: true, - eqeqeq: true, - immed: true, - latedef: "nofunc", - newcap: true, - noarg: true, - sub: true, - boss: true, - //NOTE: This is required so it doesn't barf on reserved words like delete when doing $http.delete - es5: true, - eqnull: true, - //NOTE: we need to use eval sometimes so ignore it - evil: true, - //NOTE: we need to check for strings such as "javascript:" so don't throw errors regarding those - scripturl: true, - //NOTE: we ignore tabs vs spaces because enforcing that causes lots of errors depending on the text editor being used - smarttabs: true, - globalstrict: true, - globals: { $: false, jQuery: false, define: false, require: false, window: false } - } - } - }, - - bower: { - dev: { - dest: '<%= distdir %>/lib', - options: { - expand: true, - ignorePackages: ['bootstrap'], - packageSpecific: { - 'moment': { - keepExpandedHierarchy: false, - files: ['min/moment-with-locales.js'] - }, - 'typeahead.js': { - keepExpandedHierarchy: false, - files: ['dist/typeahead.bundle.min.js'] - }, - 'underscore': { - files: ['underscore-min.js', 'underscore-min.map'] - }, - 'rgrove-lazyload': { - files: ['lazyload.js'] - }, - 'bootstrap-social': { - files: ['bootstrap-social.css'] - }, - 'font-awesome': { - files: ['css/font-awesome.min.css', 'fonts/*'] - }, - "jquery": { - 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'] - }, - 'angular-dynamic-locale': { - files: ['tmhDynamicLocale.min.js', 'tmhDynamicLocale.min.js.map'] - }, - 'ng-file-upload': { - keepExpandedHierarchy: false, - files: ['ng-file-upload.min.js'] - }, - 'angular-local-storage': { - keepExpandedHierarchy: false, - files: ['dist/angular-local-storage.min.js'] - }, - 'codemirror': { - files: [ - 'lib/codemirror.js', - 'lib/codemirror.css', - - 'mode/css/*', - 'mode/javascript/*', - 'mode/xml/*', - 'mode/htmlmixed/*', - - 'addon/search/*', - 'addon/edit/*', - 'addon/selection/*', - 'addon/dialog/*' - ] - }, - 'ace-builds': { - files: [ - 'src-min-noconflict/ace.js', - - 'src-min-noconflict/ext-language_tools.js', - 'src-min-noconflict/ext-searchbox.js', - 'src-min-noconflict/ext-settings_menu.js', - - 'src-min-noconflict/snippets/text.js', - 'src-min-noconflict/snippets/javascript.js', - - 'src-min-noconflict/theme-chrome.js', - - 'src-min-noconflict/mode-razor.js', - 'src-min-noconflict/mode-javascript.js', - - 'src-min-noconflict/worker-javascript.js', - ] - }, - 'clipboard': { - keepExpandedHierarchy: false, - files: ['dist/clipboard.min.js'] - } - } - } - }, - options: { - expand: true - } - }, - - "bower-install-simple": { - options: { - color: true - }, - "dev": {} - } - }); - - - - grunt.loadNpmTasks('grunt-contrib-concat'); - grunt.loadNpmTasks('grunt-contrib-jshint'); - grunt.loadNpmTasks('grunt-contrib-clean'); - grunt.loadNpmTasks('grunt-contrib-copy'); - grunt.loadNpmTasks('grunt-contrib-uglify'); - grunt.loadNpmTasks('grunt-contrib-watch'); - grunt.loadNpmTasks('grunt-recess'); - grunt.loadNpmTasks('grunt-postcss'); - - grunt.loadNpmTasks('grunt-karma'); - - grunt.loadNpmTasks('grunt-open'); - grunt.loadNpmTasks('grunt-contrib-connect'); - grunt.loadNpmTasks("grunt-bower-install-simple"); - grunt.loadNpmTasks('grunt-bower'); - grunt.loadNpmTasks('grunt-ngdocs'); - - grunt.loadNpmTasks('grunt-eslint'); - grunt.loadNpmTasks('grunt-hustler'); -}; diff --git a/src/Umbraco.Web.UI.Client/gulpfile.js b/src/Umbraco.Web.UI.Client/gulpfile.js new file mode 100644 index 0000000000..be72090661 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/gulpfile.js @@ -0,0 +1,389 @@ +var gulp = require('gulp'); +var watch = require('gulp-watch'); +var concat = require('gulp-concat'); +var rename = require('gulp-rename'); +var wrap = require("gulp-wrap-js"); +var sort = require('gulp-sort'); +var connect = require('gulp-connect'); +var open = require('gulp-open'); +var runSequence = require('run-sequence'); + +var _ = require('lodash'); +var MergeStream = require('merge-stream'); + +//Less + css +var postcss = require('gulp-postcss'); +var less = require('gulp-less'); +var autoprefixer = require('autoprefixer'); +var cssnano = require('cssnano'); + +// Documentation +var gulpDocs = require('gulp-ngdocs'); + +// Testing +var karmaServer = require('karma').Server; + +/*************************************************************** +Helper functions +***************************************************************/ +function processJs(files, out) { + + return gulp.src(files) + .pipe(sort()) + .pipe(concat(out)) + .pipe(wrap('(function(){\n%= body %\n})();')) + .pipe(gulp.dest(root + targets.js)); + + console.log(out + " compiled"); +} + +function processLess(files, out) { + + var processors = [ + autoprefixer, + cssnano + ]; + + return gulp.src(files) + .pipe(less()) + .pipe(postcss(processors)) + .pipe(rename(out)) + .pipe(gulp.dest(root + targets.css)); + + console.log(out + " compiled"); +} + +/*************************************************************** +Paths and destinations +Each group is iterated automatically in the setup tasks below +***************************************************************/ +var sources = { + + //less files used by backoffice and preview + //processed in the less task + less: { + installer: { files: ["src/less/installer.less"], out: "installer.css" }, + nonodes: { files: ["src/less/pages/nonodes.less"], out: "nonodes.style.min.css"}, + preview: { files: ['src/less/canvas-designer.less', 'src/less/helveticons.less'], out: "canvasdesigner.css" }, + umbraco: { files: ["src/less/belle.less"], out: "umbraco.css" } + }, + + //js files for backoffie + //processed in the js task + js: { + preview: { files: ["src/canvasdesigner/**/*.js"], out: "umbraco.canvasdesigner.js" }, + installer: { files: ["src/installer/**/*.js"], out: "umbraco.installer.js" }, + + controllers: { files: ["src/{views,controllers}/**/*.controller.js"], out: "umbraco.controllers.js" }, + directives: { files: ["src/common/directives/**/*.js"], out: "umbraco.directives.js" }, + filters: { files: ["src/common/filters/**/*.js"], out: "umbraco.filters.js" }, + resources: { files: ["src/common/resources/**/*.js"], out: "umbraco.resources.js" }, + services: { files: ["src/common/services/**/*.js"], out: "umbraco.services.js" }, + security: { files: ["src/common/security/**/*.js"], out: "umbraco.security.js" } + }, + + //selectors for copying all views into the build + //processed in the views task + views:{ + umbraco: {files: ["src/views/**/*html"], folder: ""}, + preview: { files: ["src/canvasdesigner/**/*.html"], folder: "../preview"}, + installer: {files: ["src/installer/steps/*.html"], folder: "install"} + }, + + //globs for file-watching + globs:{ + views: "./src/views/**/*.html", + less: "./src/less/**/*.less", + js: "./src/*.js", + lib: "./lib/**/*", + bower: "./lib-bower/**/*", + assets: "./src/assets/**" + } +}; + +var root = "../Umbraco.Web.UI/Umbraco/"; +var targets = { + js: "js/", + lib: "lib/", + views: "views/", + css: "assets/css/", + assets: "assets/" +}; + + +/************************** + * Main tasks for the project to prepare backoffice files + **************************/ + + // Build - build the files ready for production +gulp.task('build', function(cb) { + runSequence(["dependencies", "js", "less", "views"], "test:unit", cb); +}); + +// Dev - build the files ready for development and start watchers +gulp.task('dev', function(cb) { + runSequence(["dependencies", "js", "less", "views"], "watch", cb); +}); + +// Docserve - build and open the back office documentation +gulp.task('docserve', function(cb) { + runSequence('docs', 'connect:docs', 'open:docs', cb); +}); + +/************************** + * Task processes and copies all dependencies, either installed by bower, npm or stored locally in the project + **************************/ +gulp.task('dependencies', function () { + + //bower component specific copy rules + //this is to patch the sometimes wonky rules these libs are distrbuted under + + //as we do multiple things in this task, we merge the multiple streams + var stream = new MergeStream(); + + //Tinymce + stream.add( + gulp.src(["./bower_components/tinymce/plugins/**", + "./bower_components/tinymce/themes/**"], + { base: "./bower_components/tinymce/" }) + .pipe(gulp.dest(root + targets.lib + "/tinymce")) + ); + + //font-awesome + stream.add( + gulp.src(["./bower_components/font-awesome/fonts/*", + "./bower_components/font-awesome/css/font-awesome.min.css"], + { base: "./bower_components/font-awesome/" }) + .pipe(gulp.dest(root + targets.lib + "/font-awesome")) + ); + + // ace Editor + stream.add( + gulp.src(["bower_components/ace-builds/src-min-noconflict/ace.js", + "bower_components/ace-builds/src-min-noconflict/ext-language_tools.js", + "bower_components/ace-builds/src-min-noconflict/ext-searchbox.js", + "bower_components/ace-builds/src-min-noconflict/ext-settings_menu.js", + "bower_components/ace-builds/src-min-noconflict/snippets/text.js", + "bower_components/ace-builds/src-min-noconflict/snippets/javascript.js", + "bower_components/ace-builds/src-min-noconflict/theme-chrome.js", + "bower_components/ace-builds/src-min-noconflict/mode-razor.js", + "bower_components/ace-builds/src-min-noconflict/mode-javascript.js", + "bower_components/ace-builds/src-min-noconflict/worker-javascript.js"], + { base: "./bower_components/ace-builds/" }) + .pipe(gulp.dest(root + targets.lib + "/ace-builds")) + ); + + // code mirror + stream.add( + gulp.src([ + "bower_components/codemirror/lib/codemirror.js", + "bower_components/codemirror/lib/codemirror.css", + + "bower_components/codemirror/mode/css/*", + "bower_components/codemirror/mode/javascript/*", + "bower_components/codemirror/mode/xml/*", + "bower_components/codemirror/mode/htmlmixed/*", + + "bower_components/codemirror/addon/search/*", + "bower_components/codemirror/addon/edit/*", + "bower_components/codemirror/addon/selection/*", + "bower_components/codemirror/addon/dialog/*"], + { base: "./bower_components/codemirror/" }) + .pipe(gulp.dest(root + targets.lib + "/codemirror")) + ); + + //copy over libs which are not on bower (/lib) and + //libraries that have been managed by bower-installer (/lib-bower) + stream.add( + gulp.src(sources.globs.lib) + .pipe(gulp.dest(root + targets.lib)) + ); + + stream.add( + gulp.src(sources.globs.bower) + .pipe(gulp.dest(root + targets.lib)) + ); + + //Copies all static assets into /root / assets folder + //css, fonts and image files + stream.add( + gulp.src(sources.globs.assets) + .pipe(gulp.dest(root + targets.assets)) + ); + + // Copies all the less files related to the preview into their folder + //these are not pre-processed as preview has its own less combiler client side + stream.add( + gulp.src("src/canvasdesigner/editors/*.less") + .pipe(gulp.dest(root + targets.assets + "/less")) + ); + + return stream; +}); + + +/************************** + * Copies all angular JS files into their seperate umbraco.*.js file + **************************/ +gulp.task('js', function () { + + //we run multiple streams, so merge them all together + var stream = new MergeStream(); + + stream.add( + gulp.src(sources.globs.js) + .pipe(gulp.dest(root + targets.js)) + ); + + _.forEach(sources.js, function (group) { + stream.add (processJs(group.files, group.out) ); + }); + + return stream; +}); + +gulp.task('less', function () { + + var stream = new MergeStream(); + + _.forEach(sources.less, function (group) { + stream.add( processLess(group.files, group.out) ); + }); + + return stream; +}); + + +gulp.task('views', function () { + + var stream = new MergeStream(); + + _.forEach(sources.views, function (group) { + + console.log("copying " + group.files + " to " + root + targets.views + group.folder) + + stream.add ( + gulp.src(group.files) + .pipe( gulp.dest(root + targets.views + group.folder) ) + ); + + }); + + return stream; +}); + + +gulp.task('watch', function () { + + var stream = new MergeStream(); + var watchInterval = 500; + + //Setup a watcher for all groups of javascript files + _.forEach(sources.js, function (group) { + + if(group.watch !== false){ + + stream.add( + + watch(group.files, { ignoreInitial: true, interval: watchInterval }, function (file) { + + console.info(file.path + " has changed, added to: " + group.out); + processJs(group.files, group.out); + + }) + + ); + + } + + }); + + stream.add( + //watch all less files and trigger the less task + watch(sources.globs.less, { ignoreInitial: true, interval: watchInterval }, function () { + gulp.run(['less']); + }) + ); + + //watch all views - copy single file changes + stream.add( + watch(sources.globs.views, { interval: watchInterval }) + .pipe(gulp.dest(root + targets.views)) + ); + + //watch all app js files that will not be merged - copy single file changes + stream.add( + watch(sources.globs.js, { interval: watchInterval }) + .pipe(gulp.dest(root + targets.js)) + ); + + return stream; +}); + +/************************** + * Build Backoffice UI API documentation + **************************/ +gulp.task('docs', [], function (cb) { + + var options = { + html5Mode: false, + startPage: '/api', + title: "Umbraco Backoffice UI API Documentation", + dest: 'docs/api', + styles: ['docs/umb-docs.css'], + image: "https://our.umbraco.org/assets/images/logo.svg" + } + + return gulpDocs.sections({ + api: { + glob: ['src/common/**/*.js', 'docs/src/api/**/*.ngdoc'], + api: true, + title: 'API Documentation' + } + }) + .pipe(gulpDocs.process(options)) + .pipe(gulp.dest('docs/api')); + cb(); +}); + +gulp.task('connect:docs', function (cb) { + connect.server({ + root: 'docs/api', + livereload: true, + fallback: 'docs/api/index.html', + port: 8880 + }); + cb(); +}); + +gulp.task('open:docs', function (cb) { + + var options = { + uri: 'http://localhost:8880/index.html' + }; + + gulp.src(__filename) + .pipe(open(options)); + cb(); +}); + +/************************** + * Build tests + **************************/ + + // Karma test +gulp.task('test:unit', function() { + new karmaServer({ + configFile: __dirname + "/test/config/karma.conf.js", + keepalive: true + }) + .start(); +}); + +gulp.task('test:e2e', function() { + new karmaServer({ + configFile: __dirname + "/test/config/e2e.js", + keepalive: true + }) + .start(); +}); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/lib/bootstrap/less/navbar.less b/src/Umbraco.Web.UI.Client/lib/bootstrap/less/navbar.less index a448255731..b4e1edf65f 100644 --- a/src/Umbraco.Web.UI.Client/lib/bootstrap/less/navbar.less +++ b/src/Umbraco.Web.UI.Client/lib/bootstrap/less/navbar.less @@ -193,10 +193,10 @@ // Reset container width // Required here as we reset the width earlier on and the grid mixins don't override early enough -.navbar-static-top .container, +.navbar-static-top .container, .navbar-fixed-top .container, -.navbar-fixed-bottom .container { - #grid > .core > .span(@gridColumns); +.navbar-fixed-bottom .container { +width: (@gridColumnWidth * @gridColumns) + (@gridGutterWidth * (@gridColumns - 1)); } // Fixed to top diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index d2af0e2b83..98b1102aa4 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -6,7 +6,7 @@ "license": "MIT", "repository": { "type": "git", - "url": "git@github.com:umbraco/umbraco-cms.git" + "url": "https://github.com/umbraco/Umbraco-CMS.git" }, "bugs": { "url": "https://issues.umbraco.org" @@ -14,38 +14,35 @@ "engines": { "node": ">= 0.8.4" }, + "scripts": { + "install": "bower-installer", + "test": "karma start test/config/karma.conf.js --singlerun", + "build": "gulp" + }, "dependencies": {}, "devDependencies": { - "autoprefixer-core": "~5.2.1", - "bower": "^1.4.1", - "eslint": "^0.23.0", - "eslint-plugin-angular": "0.0.13", - "grunt": "~0.4.0", - "grunt-bower": "^0.19.0", - "grunt-bower-install-simple": "^1.1.3", - "grunt-contrib-clean": "~0.4.0", - "grunt-contrib-concat": "~0.1.3", - "grunt-contrib-connect": "~0.3.0", - "grunt-contrib-copy": "~0.7.0", - "grunt-contrib-jshint": "~0.2.0", - "grunt-contrib-uglify": "~0.1.1", - "grunt-contrib-watch": "~0.3.1", - "grunt-eslint": "^15.0.0", - "grunt-html2js": "~0.1.0", - "grunt-hustler": "^4.0.6", - "grunt-karma": "~0.5", - "grunt-ngdocs": "~0.1.2", - "grunt-open": "~0.2.0", - "grunt-postcss": "~0.6.0", - "grunt-recess": "~0.3", - "karma": "~0.9", - "karma-chrome-launcher": "0.0.2", - "karma-coffee-preprocessor": "0.0.1", - "karma-firefox-launcher": "0.0.2", - "karma-jasmine": "0.0.1", - "karma-phantomjs-launcher": "0.0.2", - "karma-requirejs": "0.0.1", - "karma-script-launcher": "0.0.1", - "phantomjs": "~1.9.1-0" + "autoprefixer": "^6.5.0", + "bower-installer": "^1.2.0", + "cssnano": "^3.7.6", + "gulp": "^3.9.1", + "gulp-concat": "^2.6.0", + "gulp-connect": "^5.0.0", + "gulp-less": "^3.1.0", + "gulp-ngdocs": "^0.3.0", + "gulp-open": "^2.0.0", + "gulp-postcss": "^6.2.0", + "gulp-rename": "^1.2.2", + "gulp-sort": "^2.0.0", + "gulp-watch": "^4.3.10", + "gulp-wrap": "^0.13.0", + "gulp-wrap-js": "^0.4.1", + "jasmine-core": "^2.5.2", + "karma": "^1.7.0", + "karma-jasmine": "^1.1.0", + "karma-phantomjs-launcher": "^1.0.4", + "less": "^2.6.1", + "lodash": "^4.16.3", + "merge-stream": "^1.0.1", + "run-sequence": "^2.1.0" } } diff --git a/src/Umbraco.Web.UI.Client/src/canvasdesigner.loader.js b/src/Umbraco.Web.UI.Client/src/canvasdesigner.loader.js index 59d5a4bef5..eefb5beed6 100644 --- a/src/Umbraco.Web.UI.Client/src/canvasdesigner.loader.js +++ b/src/Umbraco.Web.UI.Client/src/canvasdesigner.loader.js @@ -11,8 +11,7 @@ LazyLoad.js([ '../js/umbraco.security.js', '../ServerVariables', '../lib/spectrum/spectrum.js', - - '../js/canvasdesigner.panel.js', + '../js/umbraco.canvasdesigner.js', ], function () { jQuery(document).ready(function () { angular.bootstrap(document, ['Umbraco.canvasdesigner']); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/users/changepassword.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/users/changepassword.directive.js index f2834e4a36..d9a531c382 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/users/changepassword.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/users/changepassword.directive.js @@ -17,6 +17,8 @@ } */ + $scope.showReset = false; + //set defaults if they are not available if ($scope.config.disableToggle === undefined) { $scope.config.disableToggle = false; @@ -36,13 +38,13 @@ if ($scope.config.minPasswordLength === undefined) { $scope.config.minPasswordLength = 0; } - + //set the model defaults if (!angular.isObject($scope.passwordValues)) { //if it's not an object then just create a new one $scope.passwordValues = { newPassword: null, - oldPassword: null, + oldPassword: null, reset: null, answer: null }; @@ -61,11 +63,11 @@ //the value to compare to match passwords if (!isNew) { - $scope.confirm = ""; + $scope.passwordValues.confirm = ""; } else if ($scope.passwordValues.newPassword && $scope.passwordValues.newPassword.length > 0) { //if it is new and a new password has been set, then set the confirm password too - $scope.confirm = $scope.passwordValues.newPassword; + $scope.passwordValues.confirm = $scope.passwordValues.newPassword; } } @@ -86,6 +88,7 @@ $scope.changing = true; //if there was a previously generated password displaying, clear it $scope.passwordValues.generatedPassword = null; + $scope.passwordValues.confirm = null; }; $scope.cancelChange = function () { @@ -120,25 +123,13 @@ unsubscribe[u](); } }); - - $scope.showReset = function () { - return $scope.config.enableReset; - }; - + $scope.showOldPass = function () { return $scope.config.hasPassword && !$scope.config.allowManuallyChangingPassword && - !$scope.config.enablePasswordRetrieval && !$scope.passwordValues.reset; + !$scope.config.enablePasswordRetrieval && !$scope.showReset; }; - - $scope.showNewPass = function () { - return !$scope.passwordValues.reset; - }; - - $scope.showConfirmPass = function () { - return !$scope.passwordValues.reset; - }; - + //TODO: I don't think we need this or the cancel button, this can be up to the editor rendering this directive $scope.showCancelBtn = function () { return $scope.config.disableToggle !== true && $scope.config.hasPassword; diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/currentuser.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/currentuser.resource.js index fadbb88bb7..233d510c3e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/currentuser.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/currentuser.resource.js @@ -5,48 +5,54 @@ * * **/ -function currentUserResource($q, $http, umbRequestHelper) { +function currentUserResource($q, $http, umbRequestHelper, umbDataFormatter) { - //the factory object returned - return { + //the factory object returned + return { - performSetInvitedUserPassword: function (newPassword) { + performSetInvitedUserPassword: function (newPassword) { - if (!newPassword) { - return angularHelper.rejectedPromise({ errorMsg: 'newPassword cannot be empty' }); - } + if (!newPassword) { + return angularHelper.rejectedPromise({ errorMsg: 'newPassword cannot be empty' }); + } - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "currentUserApiBaseUrl", - "PostSetInvitedUserPassword"), - angular.toJson(newPassword)), - 'Failed to change password'); - }, + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "currentUserApiBaseUrl", + "PostSetInvitedUserPassword"), + angular.toJson(newPassword)), + 'Failed to change password'); + }, - /** - * @ngdoc method - * @name umbraco.resources.currentUserResource#changePassword - * @methodOf umbraco.resources.currentUserResource - * - * @description - * Changes the current users password - * - * @returns {Promise} resourcePromise object containing the user array. - * - */ - changePassword: function (changePasswordArgs) { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "currentUserApiBaseUrl", - "PostChangePassword"), - changePasswordArgs), - 'Failed to change password'); - } - - }; + /** + * @ngdoc method + * @name umbraco.resources.currentUserResource#changePassword + * @methodOf umbraco.resources.currentUserResource + * + * @description + * Changes the current users password + * + * @returns {Promise} resourcePromise object containing the user array. + * + */ + changePassword: function (changePasswordArgs) { + + changePasswordArgs = umbDataFormatter.formatChangePasswordModel(changePasswordArgs); + if (!changePasswordArgs) { + throw 'No password data to change'; + } + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "currentUserApiBaseUrl", + "PostChangePassword"), + changePasswordArgs), + 'Failed to change password'); + } + + }; } angular.module('umbraco.resources').factory('currentUserResource', currentUserResource); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js index 8753bb5316..86de5b586d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js @@ -87,6 +87,7 @@ function tinyMceService(dialogService, $log, imageHelper, $http, $timeout, macro editor.addButton('umbmediapicker', { icon: 'custom icon-picture', tooltip: 'Media Picker', + stateSelector: 'img', onclick: function () { var selectedElm = editor.selection.getNode(), diff --git a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js index 75c7b02d55..5fc2416927 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js @@ -7,8 +7,30 @@ * @description A helper object used to format/transform JSON Umbraco data, mostly used for persisting data to the server **/ function umbDataFormatter() { + return { + formatChangePasswordModel: function(model) { + if (!model) { + return null; + } + var trimmed = _.omit(model, ["confirm", "generatedPassword"]) + + //ensure that the pass value is null if all child properties are null + var allNull = true; + var vals = _.values(trimmed); + for (var k = 0; k < vals.length; k++) { + if (vals[k] !== null && vals[k] !== undefined) { + allNull = false; + } + } + if (allNull) { + return null; + } + + return trimmed; + }, + formatContentTypePostData: function (displayModel, action) { //create the save model from the display model @@ -82,6 +104,7 @@ //create the save model from the display model var saveModel = _.pick(displayModel, 'id', 'parentId', 'name', 'username', 'culture', 'email', 'startContentIds', 'startMediaIds', 'userGroups', 'message', 'changePassword'); + saveModel.changePassword = this.formatChangePasswordModel(saveModel.changePassword); //make sure the userGroups are just a string array var currGroups = saveModel.userGroups; @@ -221,7 +244,8 @@ }); saveModel.email = propEmail.value; saveModel.username = propLogin.value; - saveModel.password = propPass.value; + + saveModel.password = this.formatChangePasswordModel(propPass.value); var selectedGroups = []; for (var n in propGroups.value) { diff --git a/src/Umbraco.Web.UI.Client/src/installer/_module.js b/src/Umbraco.Web.UI.Client/src/installer/_module.js index 03ba93c846..76393089ce 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/_module.js +++ b/src/Umbraco.Web.UI.Client/src/installer/_module.js @@ -1 +1 @@ -angular.module("umbraco.install", ["umbraco.directives"]); \ No newline at end of file +angular.module("umbraco.install", ["umbraco.directives"]); \ No newline at end of file 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 8348b072b1..f6879bb679 100644 --- a/src/Umbraco.Web.UI.Client/src/less/application/grid.less +++ b/src/Umbraco.Web.UI.Client/src/less/application/grid.less @@ -125,7 +125,7 @@ body { @media (max-width: 500px) { #search-form .form-search { - width: ~"(calc(~'100%' - ~'80px'))"; + width: calc(100% - 80px); } } diff --git a/src/Umbraco.Web.UI.Client/src/less/canvas-designer.less b/src/Umbraco.Web.UI.Client/src/less/canvas-designer.less index e7aa9d859f..095027d86b 100644 --- a/src/Umbraco.Web.UI.Client/src/less/canvas-designer.less +++ b/src/Umbraco.Web.UI.Client/src/less/canvas-designer.less @@ -14,7 +14,7 @@ body { overflow: hidden; height: 100%; width: 100%; - width: ~"(calc(~'100%' - ~'80px'))"; // 80px is the fixed left menu for toggling the different browser sizes + width: calc(100% - 80px); // 80px is the fixed left menu for toggling the different browser sizes position: absolute; padding: 0; margin: 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 c7e91c1a44..0a8dfe6f71 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/overlays.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/overlays.less @@ -155,7 +155,7 @@ @media (max-width: 500px) { .umb-overlay.umb-overlay-left { margin-left: 41px; - width: ~"(calc(~'100%' - ~'41px'))"; + width: calc(100% - 41px); } } 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 9f9d288f09..85c8ae61e1 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 @@ -329,7 +329,7 @@ a.umb-package-details__back-link { .umb-package-details__main-content { flex: 1 1 auto; margin-right: 30px; - width: ~"(calc(~'100%' - ~'@{sidebarwidth}' - ~'30px'))"; // Make sure that the main content area doesn't gets affected by inline styling + width: calc(~'100%' - ~'@{sidebarwidth}' - ~'30px'); // Make sure that the main content area doesn't gets affected by inline styling } .umb-package-details__sidebar { diff --git a/src/Umbraco.Web.UI.Client/src/less/panel.less b/src/Umbraco.Web.UI.Client/src/less/panel.less index 378f6228a6..8005d37ba8 100644 --- a/src/Umbraco.Web.UI.Client/src/less/panel.less +++ b/src/Umbraco.Web.UI.Client/src/less/panel.less @@ -356,7 +356,7 @@ .umb-panel-header-content-wrapper { display: flex; flex-direction: column; - height: 100px; + height: 99px; padding: 0 20px; } diff --git a/src/Umbraco.Web.UI.Client/src/less/sections.less b/src/Umbraco.Web.UI.Client/src/less/sections.less index e90a053a3d..03d25a78f5 100644 --- a/src/Umbraco.Web.UI.Client/src/less/sections.less +++ b/src/Umbraco.Web.UI.Client/src/less/sections.less @@ -131,7 +131,7 @@ ul.sections li.help { bottom: 0; left: 0; display: block; - width: ~"(calc(~'100%' - ~'5px'))"; //subtract 4px orange border + 1px border-right for sections + width: calc(100% - 5px); //subtract 4px orange border + 1px border-right for sections } ul.sections li.help a { 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 3af0c30a08..58701f20f0 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 @@ -13,7 +13,9 @@

Hi, {{invitedUser.name}}

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non libero vel turpis ultrices pharetra.

+

+ Welcome to Umbraco! Just need to get your password and avatar setup and then you're good to go +

- + + no-dirty-check + ng-change="$parent.$parent.showReset = !$parent.$parent.showReset"/> @@ -33,7 +34,7 @@ - + - - +
+ diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js index 7198166c25..21f9534848 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js @@ -26,6 +26,7 @@ angular.module("umbraco") $scope.control.value = { focalPoint: selectedImage.focalPoint, id: selectedImage.id, + udi: selectedImage.udi, image: selectedImage.image, altText: selectedImage.altText }; diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js index 57477eebeb..ec072b9268 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js @@ -15,7 +15,7 @@ vm.labels = {}; vm.maxFileSize = Umbraco.Sys.ServerVariables.umbracoSettings.maxFileSize + "KB"; vm.acceptedFileTypes = mediaHelper.formatFileTypes(Umbraco.Sys.ServerVariables.umbracoSettings.imageFileTypes); - vm.emailIsUsername = true; + vm.usernameIsEmail = Umbraco.Sys.ServerVariables.umbracoSettings.usernameIsEmail; //create the initial model for change password vm.changePasswordModel = { @@ -68,7 +68,7 @@ setUserDisplayState(); formatDatesToLocal(vm.user); - vm.emailIsUsername = user.email === user.username; + vm.usernameIsEmail = Umbraco.Sys.ServerVariables.umbracoSettings.usernameIsEmail && user.email === user.username; //go get the config for the membership provider and add it to the model authResource.getMembershipProviderConfig().then(function (data) { diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.html b/src/Umbraco.Web.UI.Client/src/views/users/user.html index ebe493fce8..e043b50d2c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.html @@ -43,11 +43,11 @@ - +
diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js index 3ea6c46580..96ebf4f07b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js @@ -26,6 +26,8 @@ vm.selectedBulkUserGroups = []; + vm.usernameIsEmail = Umbraco.Sys.ServerVariables.umbracoSettings.usernameIsEmail; + vm.allowDisableUser = true; vm.allowEnableUser = true; vm.allowUnlockUser = true; @@ -50,24 +52,31 @@ "selected": true }; - //don't set this if no email is configured - if (Umbraco.Sys.ServerVariables.umbracoSettings.emailServerConfigured) { + //don't show the invite button if no email is configured + if (Umbraco.Sys.ServerVariables.umbracoSettings.showUserInvite) { vm.defaultButton = { labelKey: "user_inviteUser", - handler: function () { + handler: function() { vm.setUsersViewState('inviteUser'); } }; + vm.subButtons = [ + { + labelKey: "user_createUser", + handler: function () { + vm.setUsersViewState('createUser'); + } + } + ]; } - - vm.subButtons = [ - { + else { + vm.defaultButton = { labelKey: "user_createUser", handler: function () { vm.setUsersViewState('createUser'); } - } - ]; + }; + } vm.toggleFilter = toggleFilter; vm.setUsersViewState = setUsersViewState; diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html index 1d07e9db7e..e4fd2d24be 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html @@ -325,6 +325,13 @@ + + + Required + + + diff --git a/src/Umbraco.Web.UI.Client/test/config/karma.conf.js b/src/Umbraco.Web.UI.Client/test/config/karma.conf.js index 0e06085ff4..024a014245 100644 --- a/src/Umbraco.Web.UI.Client/test/config/karma.conf.js +++ b/src/Umbraco.Web.UI.Client/test/config/karma.conf.js @@ -1,5 +1,7 @@ -module.exports = function(karma) { - karma.configure({ +module.exports = function(config) { + + config.set({ + // base path, that will be used to resolve files and exclude basePath: '../..', @@ -7,31 +9,23 @@ module.exports = function(karma) { // list of files / patterns to load in the browser files: [ - 'lib/../build/belle/lib/jquery/jquery.min.js', + + //libraries + 'lib-bower/jquery/jquery.min.js', 'lib/angular/1.1.5/angular.js', 'lib/angular/1.1.5/angular-cookies.min.js', 'lib/angular/1.1.5/angular-mocks.js', 'lib/angular/angular-ui-sortable.js', - - /* - For angular 1.2: - 'lib/angular/1.2/angular.js', - 'lib/angular/1.2/angular-route.min.js', - 'lib/angular/1.2/angular-touch.min.js', - 'lib/angular/1.2/angular-cookies.min.js', - 'lib/angular/1.2/angular-animate.min.js', - 'lib/angular/1.2/angular-mocks.js',*/ - - - 'lib/../build/belle/lib/underscore/underscore-min.js', - 'lib/../build/belle/lib/moment/moment-with-locales.js', + 'lib-bower/underscore/underscore-min.js', + 'lib-bower/moment/moment-with-locales.js', 'lib/umbraco/Extensions.js', - 'lib/../build/belle/lib/rgrove-lazyload/lazyload.js', - 'lib/../build/belle/lib/angular-local-storage/angular-local-storage.min.js', + 'lib-bower/rgrove-lazyload/lazyload.js', + 'lib-bower//angular-local-storage/angular-local-storage.min.js', + //app bootstrap and loader 'test/config/app.unit.js', - 'src/common/mocks/umbraco.servervariables.js', + //application files 'src/common/directives/**/*.js', 'src/common/filters/*.js', 'src/common/services/*.js', @@ -39,8 +33,9 @@ module.exports = function(karma) { 'src/common/resources/*.js', 'src/common/mocks/**/*.js', 'src/views/**/*.controller.js', - 'test/unit/**/*.spec.js', - {pattern: 'lib/**/*.js', watched: true, served: true, included: false} + + //tests + 'test/unit/**/*.spec.js' ], // list of files to exclude @@ -66,7 +61,7 @@ module.exports = function(karma) { // level of logging // possible values: karma.LOG_DISABLE || karma.LOG_ERROR || karma.LOG_WARN || karma.LOG_INFO || karma.LOG_DEBUG // CLI --log-level debug - logLevel: karma.LOG_INFO, + logLevel: config.LOG_WARN, // enable / disable watching file and executing tests whenever any file changes // CLI --auto-watch --no-auto-watch diff --git a/src/Umbraco.Web.UI/Umbraco/js/install.js b/src/Umbraco.Web.UI/Umbraco/js/install.js deleted file mode 100644 index 41aab12c80..0000000000 --- a/src/Umbraco.Web.UI/Umbraco/js/install.js +++ /dev/null @@ -1,30 +0,0 @@ -yepnope({ - - load: [ - 'lib/jquery/jquery-2.0.3.min.js', - - /* 1.1.5 */ - 'lib/angular/1.1.5/angular.min.js', - 'lib/angular/1.1.5/angular-cookies.min.js', - 'lib/angular/1.1.5/angular-mobile.min.js', - 'lib/angular/1.1.5/angular-mocks.js', - 'lib/angular/1.1.5/angular-sanitize.min.js', - 'lib/underscore/underscore.js', - 'js/umbraco.servervariables.js', - 'js/app.dev.js' - ], - - complete: function () { - jQuery(document).ready(function () { - - angular.module('umbraco.install', [ - 'umbraco.resources', - 'umbraco.services', - 'umbraco.httpbackend', - 'ngMobile' - ]); - - angular.bootstrap(document, ['umbraco.install']); - }); - } -}); \ No newline at end of file diff --git a/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap2.cshtml b/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap2.cshtml index c8f9ab7cd1..8b189ae1a0 100644 --- a/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap2.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap2.cshtml @@ -2,10 +2,6 @@ @using Umbraco.Web.Templates @using Newtonsoft.Json.Linq -@* - Razor helpers located at the bottom of this file -*@ - @if (Model != null && Model.sections != null) { var oneColumn = ((System.Collections.ICollection)Model.sections).Count == 1; @@ -64,29 +60,21 @@ JObject cfg = contentItem.config; if(cfg != null) - foreach (JProperty property in cfg.Properties()) - { - var propertyValue = HttpUtility.HtmlAttributeEncode(property.Value.ToString()); - attrs.Add(property.Name + "=\"" + propertyValue + "\""); + foreach (JProperty property in cfg.Properties()) { + attrs.Add(property.Name + "='" + property.Value.ToString() + "'"); } - + JObject style = contentItem.styles; - if (style != null) { - var cssVals = new List(); - foreach (JProperty property in style.Properties()) - { - var propertyValue = property.Value.ToString(); - if (string.IsNullOrWhiteSpace(propertyValue) == false) - { - cssVals.Add(property.Name + ":" + propertyValue + ";"); - } - } + if (style != null) { + var cssVals = new List(); + foreach (JProperty property in style.Properties()) + cssVals.Add(property.Name + ":" + property.Value.ToString() + ";"); - if (cssVals.Any()) - attrs.Add("style=\"" + HttpUtility.HtmlAttributeEncode(string.Join(" ", cssVals)) + "\""); + if (cssVals.Any()) + attrs.Add("style='" + string.Join(" ", cssVals) + "'"); } - + return new MvcHtmlString(string.Join(" ", attrs)); } } \ No newline at end of file diff --git a/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3.cshtml b/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3.cshtml index 6ab5c1355a..e672aa2a11 100644 --- a/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3.cshtml @@ -2,10 +2,6 @@ @using Umbraco.Web.Templates @using Newtonsoft.Json.Linq -@* - Razor helpers located at the bottom of this file -*@ - @if (Model != null && Model.sections != null) { var oneColumn = ((System.Collections.ICollection)Model.sections).Count == 1; @@ -64,29 +60,21 @@ JObject cfg = contentItem.config; if(cfg != null) - foreach (JProperty property in cfg.Properties()) - { - var propertyValue = HttpUtility.HtmlAttributeEncode(property.Value.ToString()); - attrs.Add(property.Name + "=\"" + propertyValue + "\""); + foreach (JProperty property in cfg.Properties()) { + attrs.Add(property.Name + "='" + property.Value.ToString() + "'"); } - + JObject style = contentItem.styles; - if (style != null) { - var cssVals = new List(); - foreach (JProperty property in style.Properties()) - { - var propertyValue = property.Value.ToString(); - if (string.IsNullOrWhiteSpace(propertyValue) == false) - { - cssVals.Add(property.Name + ":" + propertyValue + ";"); - } - } + if (style != null) { + var cssVals = new List(); + foreach (JProperty property in style.Properties()) + cssVals.Add(property.Name + ":" + property.Value.ToString() + ";"); - if (cssVals.Any()) - attrs.Add("style=\"" + HttpUtility.HtmlAttributeEncode(string.Join(" ", cssVals)) + "\""); + if (cssVals.Any()) + attrs.Add("style='" + string.Join(" ", cssVals) + "'"); } - + return new MvcHtmlString(string.Join(" ", attrs)); } } \ No newline at end of file diff --git a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Base.cshtml b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Base.cshtml index ffb7603048..a86c04819a 100644 --- a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Base.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Base.cshtml @@ -1,4 +1,5 @@ @model dynamic +@using Umbraco.Web.Templates @functions { public static string EditorView(dynamic contentItem) diff --git a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Embed.cshtml b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Embed.cshtml index c27be6bcdf..393157bcf8 100644 --- a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Embed.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Embed.cshtml @@ -1,2 +1,7 @@ @model dynamic -@Html.Raw(Model.value) +@using Umbraco.Web.Templates + + +
+ @Html.Raw(Model.value) +
diff --git a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Macro.cshtml b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Macro.cshtml index ed08bb2484..e0822808d8 100644 --- a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Macro.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Macro.cshtml @@ -1,4 +1,6 @@ @inherits UmbracoViewPage +@using Umbraco.Web.Templates + @if (Model.value != null) { diff --git a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Media.cshtml b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Media.cshtml index 5b5adbdc7d..09d04219f2 100644 --- a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Media.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Media.cshtml @@ -1,4 +1,5 @@ @model dynamic +@using Umbraco.Web.Templates @if (Model.value != null) { @@ -13,7 +14,7 @@ } } - @Model.value.altText + @Model.value.caption if (Model.value.caption != null) { diff --git a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/TextString.cshtml b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/TextString.cshtml index 8c92ca0d83..0cac4eb1ff 100644 --- a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/TextString.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/TextString.cshtml @@ -4,9 +4,8 @@ @if (Model.editor.config.markup != null) { string markup = Model.editor.config.markup.ToString(); - var umbracoHelper = new UmbracoHelper(UmbracoContext.Current); - - markup = markup.Replace("#value#", umbracoHelper.ReplaceLineBreaksForHtml(HttpUtility.HtmlEncode(Model.value.ToString()))); + + markup = markup.Replace("#value#", Model.value.ToString()); markup = markup.Replace("#style#", Model.editor.config.style.ToString()); diff --git a/src/Umbraco.Web.UI/config/grid.editors.config.js b/src/Umbraco.Web.UI/config/grid.editors.config.js index b904920566..1adb6da2dc 100644 --- a/src/Umbraco.Web.UI/config/grid.editors.config.js +++ b/src/Umbraco.Web.UI/config/grid.editors.config.js @@ -1,4 +1,4 @@ -[ +[ { "name": "Rich text editor", "alias": "rte", @@ -11,6 +11,40 @@ "view": "media", "icon": "icon-picture" }, + { + "name": "Image wide", + "alias": "media_wide", + "view": "media", + "render": "/App_Plugins/Grid/Editors/Render/media_wide.cshtml", + "icon": "icon-picture" + }, + { + "name": "Image wide cropped", + "alias": "media_wide_cropped", + "view": "media", + "render": "media", + "icon": "icon-picture", + "config": { + "size": { + "width": 1920, + "height": 700 + } + } + }, + { + "name": "Image rounded", + "alias": "media_round", + "view": "media", + "render": "/App_Plugins/Grid/Editors/Render/media_round.cshtml", + "icon": "icon-picture" + }, + { + "name": "Image w/ text right", + "alias": "media_text_right", + "view": "/App_Plugins/Grid/Editors/Views/media_with_description.html", + "render": "/App_Plugins/Grid/Editors/Render/media_text_right.cshtml", + "icon": "icon-picture" + }, { "name": "Macro", "alias": "macro", @@ -21,8 +55,29 @@ "name": "Embed", "alias": "embed", "view": "embed", + "render": "/App_Plugins/Grid/Editors/Render/embed_videowrapper.cshtml", "icon": "icon-movie-alt" }, + { + "name": "Banner Headline", + "alias": "banner_headline", + "view": "textstring", + "icon": "icon-coin", + "config": { + "style": "font-size: 36px; line-height: 45px; font-weight: bold; text-align:center", + "markup": "

#value#

" + } + }, + { + "name": "Banner Tagline", + "alias": "banner_tagline", + "view": "textstring", + "icon": "icon-coin", + "config": { + "style": "font-size: 25px; line-height: 35px; font-weight: normal; text-align:center", + "markup": "

#value#

" + } + }, { "name": "Headline", "alias": "headline", @@ -32,6 +87,36 @@ "style": "font-size: 36px; line-height: 45px; font-weight: bold", "markup": "

#value#

" } + }, + { + "name": "Headline centered", + "alias": "headline_centered", + "view": "textstring", + "icon": "icon-coin", + "config": { + "style": "font-size: 30px; line-height: 45px; font-weight: bold; text-align:center;", + "markup": "

#value#

" + } + }, + { + "name": "Abstract", + "alias": "abstract", + "view": "textstring", + "icon": "icon-coin", + "config": { + "style": "font-size: 16px; line-height: 20px; font-weight: bold;", + "markup": "

#value#

" + } + }, + { + "name": "Paragraph", + "alias": "paragraph", + "view": "textstring", + "icon": "icon-font", + "config": { + "style": "font-size: 16px; line-height: 20px; font-weight: light;", + "markup": "

#value#

" + } }, { "name": "Quote", @@ -39,8 +124,28 @@ "view": "textstring", "icon": "icon-quote", "config": { - "style": "border-left: 3px solid #ccc; padding: 10px; color: #ccc; font-family: serif; font-style: italic; font-size: 18px", + "style": "border-left: 3px solid #ccc; padding: 10px; color: #ccc; font-family: serif; font-variant: italic; font-size: 18px", "markup": "
#value#
" } + }, + { + "name": "Quote with description", + "alias": "quote_D", + "view": "/App_Plugins/Grid/Editors/Views/quote_with_description.html", + "render": "/App_Plugins/Grid/Editors/Render/quote_with_description.cshtml", + "icon": "icon-quote", + "config": { + "style": "border-left: 3px solid #ccc; padding: 10px; color: #ccc; font-family: serif; font-variant: italic; font-size: 18px" + } + }, + { + "name": "Code", + "alias": "code", + "view": "textstring", + "icon": "icon-code", + "config": { + "style": "overflow: auto;padding: 6px 10px;border: 1px solid #ddd;border-radius: 3px;background-color: #f8f8f8;font-size: .9rem;font-family: 'Courier 10 Pitch', Courier, monospace;line-height: 19px;", + "markup": "
#value#
" + } } ] \ No newline at end of file diff --git a/src/Umbraco.Web.UI/config/umbracoSettings.Release.config b/src/Umbraco.Web.UI/config/umbracoSettings.Release.config index 6687e71f4c..4e0c24434c 100644 --- a/src/Umbraco.Web.UI/config/umbracoSettings.Release.config +++ b/src/Umbraco.Web.UI/config/umbracoSettings.Release.config @@ -66,6 +66,8 @@ false + + true false diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index 5d0779f8b6..77615cf11a 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -81,14 +81,14 @@ Domain '%0%' has been updated Edit Current Domains - Inherit Culture - or inherit culture from parent nodes. Will also apply
+ or inherit culture from parent nodes. Will also apply
to the current node, unless a domain below applies too.]]>
Domains @@ -339,11 +339,11 @@ Number of columns Number of rows - Set a placeholder id by setting an ID on your placeholder you can inject content into this template from child templates, + Set a placeholder id by setting an ID on your placeholder you can inject content into this template from child templates, by referring this ID using a <asp:content /> element.]]> - Select a placeholder id from the list below. You can only + Select a placeholder id from the list below. You can only choose Id's from the current template's master.]]> Click on the image to see full size @@ -380,15 +380,15 @@ - %0%' below
You can add additional languages under the 'languages' in the menu on the left + %0%' below
You can add additional languages under the 'languages' in the menu on the left ]]>
Culture Name Edit the key of the dictionary item. - @@ -398,6 +398,8 @@ Confirm your password Name the %0%... Enter a name... + Enter an email... + Enter a username... Label... Enter a description... Type to search... diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml index ed0edf8b6d..9f42eb7035 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -400,6 +400,7 @@ Name the %0%... Enter a name... Enter an email... + Enter a username... Label... Enter a description... Type to search... @@ -1661,6 +1662,7 @@ To manage your website, simply open the Umbraco back office and start adding con User groups has been invited An invitation has been sent to the new user with details about how to log in to Umbraco. + Hello there and welcome to Umbraco! In just 1 minute you’ll be good to go, we just need you to setup a password and add a picture for your avatar. Writer Translator Change diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/sv.xml b/src/Umbraco.Web.UI/umbraco/config/lang/sv.xml index de2c632b74..aa5e617cd1 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/sv.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/sv.xml @@ -636,8 +636,12 @@ Återställ + Definiera beskräning + Ge beskärningen ett alias och dess standardbredd och -höjd + spara beskärning + Lägg till ny beskärning - + Nuvarande version Röd text kommer inte att synas i den valda versionen. , Grön betyder att den har tillkommit]]> Dokumentet har återgått till en tidigare version diff --git a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs index bfb1974f2f..27fbc6d5c5 100644 --- a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs +++ b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs @@ -373,7 +373,7 @@ namespace Umbraco.Web.Cache { DistributedCache.Instance.RemoveUnpublishedPageCache(e.DeletedEntities.ToArray()); } - + /// /// Handles cache refreshing for when content is saved (not published) /// @@ -767,7 +767,6 @@ namespace Umbraco.Web.Cache { var handler = FindHandler(e); if (handler == null) continue; - handler.Invoke(null, new[] { e.Sender, e.Args }); } } diff --git a/src/Umbraco.Web/Cache/PageCacheRefresher.cs b/src/Umbraco.Web/Cache/PageCacheRefresher.cs index e884c9b3b8..d9bb7e9b88 100644 --- a/src/Umbraco.Web/Cache/PageCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/PageCacheRefresher.cs @@ -50,8 +50,9 @@ namespace Umbraco.Web.Cache ///
public override void RefreshAll() { - content.Instance.RefreshContentFromDatabase(); - XmlPublishedContent.ClearRequest(); + if (Suspendable.PageCacheRefresher.CanRefreshDocumentCacheFromDatabase) + content.Instance.RefreshContentFromDatabase(); + ClearCaches(); base.RefreshAll(); } @@ -61,11 +62,9 @@ namespace Umbraco.Web.Cache /// The id. public override void Refresh(int id) { - ApplicationContext.Current.ApplicationCache.ClearPartialViewCache(); - content.Instance.UpdateDocumentCache(id); - XmlPublishedContent.ClearRequest(); - DistributedCache.Instance.ClearAllMacroCacheOnCurrentServer(); - DistributedCache.Instance.ClearXsltCacheOnCurrentServer(); + if (Suspendable.PageCacheRefresher.CanUpdateDocumentCache) + content.Instance.UpdateDocumentCache(id); + ClearCaches(); base.Refresh(id); } @@ -75,35 +74,35 @@ namespace Umbraco.Web.Cache /// The id. public override void Remove(int id) { - ApplicationContext.Current.ApplicationCache.ClearPartialViewCache(); - content.Instance.ClearDocumentCache(id, false); - XmlPublishedContent.ClearRequest(); - DistributedCache.Instance.ClearAllMacroCacheOnCurrentServer(); - DistributedCache.Instance.ClearXsltCacheOnCurrentServer(); - ClearAllIsolatedCacheByEntityType(); + if (Suspendable.PageCacheRefresher.CanUpdateDocumentCache) + content.Instance.ClearDocumentCache(id, false); + ClearCaches(); base.Remove(id); } public override void Refresh(IContent instance) { - ApplicationContext.Current.ApplicationCache.ClearPartialViewCache(); - content.Instance.UpdateDocumentCache(new Document(instance)); - XmlPublishedContent.ClearRequest(); - DistributedCache.Instance.ClearAllMacroCacheOnCurrentServer(); - DistributedCache.Instance.ClearXsltCacheOnCurrentServer(); - ClearAllIsolatedCacheByEntityType(); + if (Suspendable.PageCacheRefresher.CanUpdateDocumentCache) + content.Instance.UpdateDocumentCache(new Document(instance)); + ClearCaches(); base.Refresh(instance); } public override void Remove(IContent instance) + { + if (Suspendable.PageCacheRefresher.CanUpdateDocumentCache) + content.Instance.ClearDocumentCache(new Document(instance), false); + ClearCaches(); + base.Remove(instance); + } + + private void ClearCaches() { ApplicationContext.Current.ApplicationCache.ClearPartialViewCache(); - content.Instance.ClearDocumentCache(new Document(instance), false); XmlPublishedContent.ClearRequest(); DistributedCache.Instance.ClearAllMacroCacheOnCurrentServer(); DistributedCache.Instance.ClearXsltCacheOnCurrentServer(); ClearAllIsolatedCacheByEntityType(); - base.Remove(instance); } } } diff --git a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs index ab41246753..3b185ef4b1 100644 --- a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs @@ -286,10 +286,11 @@ namespace Umbraco.Web.Editors GetMaxRequestLength() }, {"keepUserLoggedIn", UmbracoConfig.For.UmbracoSettings().Security.KeepUserLoggedIn}, + {"usernameIsEmail", UmbracoConfig.For.UmbracoSettings().Security.UsernameIsEmail}, {"cssPath", IOHelper.ResolveUrl(SystemDirectories.Css).TrimEnd('/')}, {"allowPasswordReset", UmbracoConfig.For.UmbracoSettings().Security.AllowPasswordReset}, {"loginBackgroundImage", UmbracoConfig.For.UmbracoSettings().Content.LoginBackgroundImage}, - {"emailServerConfigured", GlobalSettings.HasSmtpServerConfigured(_httpContext.Request.ApplicationPath)}, + {"showUserInvite", EmailSender.CanSendRequiredEmail}, } }, { diff --git a/src/Umbraco.Web/Editors/PasswordChanger.cs b/src/Umbraco.Web/Editors/PasswordChanger.cs index a6edd1bdd0..88e92c0ad2 100644 --- a/src/Umbraco.Web/Editors/PasswordChanger.cs +++ b/src/Umbraco.Web/Editors/PasswordChanger.cs @@ -98,8 +98,8 @@ namespace Umbraco.Web.Editors _logger.Warn(string.Format("Could not reset user password {0}", errors)); return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not reset password, errors: " + errors, new[] { "resetPassword" }) }); } - - return Attempt.Succeed(new PasswordChangedModel { ResetPassword = newPass }); + + return Attempt.Succeed(new PasswordChangedModel()); } //we're not resetting it so we need to try to change it. diff --git a/src/Umbraco.Web/Editors/UserGroupsController.cs b/src/Umbraco.Web/Editors/UserGroupsController.cs index 33b1d9dc08..18777395d3 100644 --- a/src/Umbraco.Web/Editors/UserGroupsController.cs +++ b/src/Umbraco.Web/Editors/UserGroupsController.cs @@ -133,7 +133,10 @@ namespace Umbraco.Web.Editors [UserGroupAuthorization("userGroupIds")] public HttpResponseMessage PostDeleteUserGroups([FromUri] int[] userGroupIds) { - var userGroups = Services.UserService.GetAllUserGroups(userGroupIds).ToArray(); + var userGroups = Services.UserService.GetAllUserGroups(userGroupIds) + //never delete the admin group + .Where(x => x.Alias != Constants.Security.AdminGroupAlias) + .ToArray(); foreach (var userGroup in userGroups) { Services.UserService.DeleteUserGroup(userGroup); diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs index cb2963124f..63484d1cec 100644 --- a/src/Umbraco.Web/Editors/UsersController.cs +++ b/src/Umbraco.Web/Editors/UsersController.cs @@ -265,13 +265,18 @@ namespace Umbraco.Web.Editors { throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState)); } - - var existing = Services.UserService.GetByEmail(userSave.Email); - if (existing != null) + + if (UmbracoConfig.For.UmbracoSettings().Security.UsernameIsEmail) { - ModelState.AddModelError("Email", "A user with the email already exists"); - throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState)); + //ensure they are the same if we're using it + userSave.Username = userSave.Email; } + else + { + //first validate the username if were showing it + CheckUniqueUsername(userSave.Username, null); + } + CheckUniqueEmail(userSave.Email, null); //Perform authorization here to see if the current user can actually save this user with the info being requested var authHelper = new UserEditorAuthorizationHelper(Services.ContentService, Services.MediaService, Services.UserService, Services.EntityService); @@ -283,7 +288,7 @@ namespace Umbraco.Web.Editors //we want to create the user with the UserManager, this ensures the 'empty' (special) password //format is applied without us having to duplicate that logic - var identityUser = BackOfficeIdentityUser.CreateNew(userSave.Email, userSave.Email, GlobalSettings.DefaultUILanguage); + var identityUser = BackOfficeIdentityUser.CreateNew(userSave.Username, userSave.Email, GlobalSettings.DefaultUILanguage); identityUser.Name = userSave.Name; var created = await UserManager.CreateAsync(identityUser); @@ -345,21 +350,26 @@ namespace Umbraco.Web.Editors { throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState)); } - - var hasSmtp = GlobalSettings.HasSmtpServerConfigured(RequestContext.VirtualPathRoot); - if (hasSmtp == false) + + if (EmailSender.CanSendRequiredEmail == false) { throw new HttpResponseException( Request.CreateNotificationValidationErrorResponse("No Email server is configured")); } - var user = Services.UserService.GetByEmail(userSave.Email); - if (user != null && (user.LastLoginDate != default(DateTime) || user.EmailConfirmedDate.HasValue)) + IUser user; + if (UmbracoConfig.For.UmbracoSettings().Security.UsernameIsEmail) { - ModelState.AddModelError("Email", "A user with the email already exists"); - throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState)); + //ensure it's the same + userSave.Username = userSave.Email; } - + else + { + //first validate the username if we're showing it + user = CheckUniqueUsername(userSave.Username, u => u.LastLoginDate != default(DateTime) || u.EmailConfirmedDate.HasValue); + } + user = CheckUniqueEmail(userSave.Email, u => u.LastLoginDate != default(DateTime) || u.EmailConfirmedDate.HasValue); + //Perform authorization here to see if the current user can actually save this user with the info being requested var authHelper = new UserEditorAuthorizationHelper(Services.ContentService, Services.MediaService, Services.UserService, Services.EntityService); var canSaveUser = authHelper.IsAuthorized(Security.CurrentUser, user, null, null, userSave.UserGroups); @@ -372,7 +382,7 @@ namespace Umbraco.Web.Editors { //we want to create the user with the UserManager, this ensures the 'empty' (special) password //format is applied without us having to duplicate that logic - var identityUser = BackOfficeIdentityUser.CreateNew(userSave.Email, userSave.Email, GlobalSettings.DefaultUILanguage); + var identityUser = BackOfficeIdentityUser.CreateNew(userSave.Username, userSave.Email, GlobalSettings.DefaultUILanguage); identityUser.Name = userSave.Name; var created = await UserManager.CreateAsync(identityUser); @@ -402,7 +412,31 @@ namespace Umbraco.Web.Editors return display; } - + + private IUser CheckUniqueEmail(string email, Func extraCheck) + { + var user = Services.UserService.GetByEmail(email); + if (user != null && (extraCheck == null || extraCheck(user))) + { + ModelState.AddModelError("Email", "A user with the email already exists"); + throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState)); + } + return user; + } + + private IUser CheckUniqueUsername(string username, Func extraCheck) + { + var user = Services.UserService.GetByUsername(username); + if (user != null && (extraCheck == null || extraCheck(user))) + { + ModelState.AddModelError( + UmbracoConfig.For.UmbracoSettings().Security.UsernameIsEmail ? "Email" : "Username", + "A user with the username already exists"); + throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState)); + } + return user; + } + private HttpContextBase EnsureHttpContext() { var attempt = this.TryGetHttpContext(); @@ -442,12 +476,15 @@ namespace Umbraco.Web.Editors UserExtensions.GetUserCulture(to.Language, Services.TextService), new[] { userDisplay.Name, from, message, inviteUri.ToString() }); - await UserManager.EmailService.SendAsync(new IdentityMessage - { - Body = emailBody, - Destination = userDisplay.Email, - Subject = emailSubject - }); + await UserManager.EmailService.SendAsync( + //send the special UmbracoEmailMessage which configures it's own sender + //to allow for events to handle sending the message if no smtp is configured + new UmbracoEmailMessage(new EmailSender(true)) + { + Body = emailBody, + Destination = userDisplay.Email, + Subject = emailSubject + }); } @@ -516,8 +553,7 @@ namespace Umbraco.Web.Editors { userSave.Username = userSave.Email; } - - var resetPasswordValue = string.Empty; + if (userSave.ChangePassword != null) { var passwordChanger = new PasswordChanger(Logger, Services.UserService); @@ -525,9 +561,6 @@ namespace Umbraco.Web.Editors var passwordChangeResult = await passwordChanger.ChangePasswordWithIdentityAsync(Security.CurrentUser, found, userSave.ChangePassword, UserManager); if (passwordChangeResult.Success) { - //depending on how the provider is configured, the password may be reset so let's store that for later - resetPasswordValue = passwordChangeResult.Result.ResetPassword; - //need to re-get the user found = Services.UserService.GetUserById(intId.Result); } @@ -551,11 +584,7 @@ namespace Umbraco.Web.Editors Services.UserService.Save(user); var display = Mapper.Map(user); - - //re-map the password reset value (if any) - if (resetPasswordValue.IsNullOrWhiteSpace() == false) - display.ResetPasswordValue = resetPasswordValue; - + display.AddSuccessNotification(Services.TextService.Localize("speechBubbles/operationSavedHeader"), Services.TextService.Localize("speechBubbles/editUserSaved")); return display; } diff --git a/src/Umbraco.Web/HealthCheck/NotificationMethods/EmailNotificationMethod.cs b/src/Umbraco.Web/HealthCheck/NotificationMethods/EmailNotificationMethod.cs index c43fb0d3a1..998525c96a 100644 --- a/src/Umbraco.Web/HealthCheck/NotificationMethods/EmailNotificationMethod.cs +++ b/src/Umbraco.Web/HealthCheck/NotificationMethods/EmailNotificationMethod.cs @@ -67,7 +67,7 @@ namespace Umbraco.Web.HealthCheck.NotificationMethods var subject = _textService.Localize("healthcheck/scheduledHealthCheckEmailSubject"); - using (var client = new SmtpClient()) + var mailSender = new EmailSender(); using (var mailMessage = new MailMessage(UmbracoConfig.For.UmbracoSettings().Content.NotificationEmailAddress, RecipientEmail, string.IsNullOrEmpty(subject) ? "Umbraco Health Check Status" : subject, @@ -77,14 +77,7 @@ namespace Umbraco.Web.HealthCheck.NotificationMethods && message.Contains("<") && message.Contains(" Validate(ValidationContext validationContext) { if (UserGroups.Any() == false) - yield return new ValidationResult("A user must be assigned to at least one group", new[] { "UserGroups" }); + yield return new ValidationResult("A user must be assigned to at least one group", new[] { "UserGroups" }); + + if (UmbracoConfig.For.UmbracoSettings().Security.UsernameIsEmail == false && Username.IsNullOrWhiteSpace()) + yield return new ValidationResult("A username cannot be empty", new[] { "Username" }); } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs index 73b28ae669..46a756e0ad 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs @@ -35,6 +35,9 @@ namespace Umbraco.Web.Scheduling { if (_appContext == null) return true; // repeat... + if (Suspendable.ScheduledPublishing.CanRun == false) + return true; // repeat, later + switch (_appContext.GetCurrentServerRole()) { case ServerRole.Slave: diff --git a/src/Umbraco.Web/Search/ExamineEvents.cs b/src/Umbraco.Web/Search/ExamineEvents.cs index 7fbbf29b89..e018bac214 100644 --- a/src/Umbraco.Web/Search/ExamineEvents.cs +++ b/src/Umbraco.Web/Search/ExamineEvents.cs @@ -24,7 +24,6 @@ namespace Umbraco.Web.Search ///
public sealed class ExamineEvents : ApplicationEventHandler { - /// /// Once the application has started we should bind to all events and initialize the providers. /// @@ -32,9 +31,9 @@ namespace Umbraco.Web.Search /// /// /// We need to do this on the Started event as to guarantee that all resolvers are setup properly. - /// + /// protected override void ApplicationStarted(UmbracoApplicationBase httpApplication, ApplicationContext applicationContext) - { + { LogHelper.Info("Initializing Examine and binding to business logic events"); var registeredProviders = ExamineManager.Instance.IndexProviderCollection @@ -46,14 +45,14 @@ namespace Umbraco.Web.Search if (registeredProviders == 0) return; - //Bind to distributed cache events - this ensures that this logic occurs on ALL servers that are taking part + //Bind to distributed cache events - this ensures that this logic occurs on ALL servers that are taking part // in a load balanced environment. CacheRefresherBase.CacheUpdated += UnpublishedPageCacheRefresherCacheUpdated; CacheRefresherBase.CacheUpdated += PublishedPageCacheRefresherCacheUpdated; CacheRefresherBase.CacheUpdated += MediaCacheRefresherCacheUpdated; CacheRefresherBase.CacheUpdated += MemberCacheRefresherCacheUpdated; CacheRefresherBase.CacheUpdated += ContentTypeCacheRefresherCacheUpdated; - + var contentIndexer = ExamineManager.Instance.IndexProviderCollection[Constants.Examine.InternalIndexer] as UmbracoContentIndexer; if (contentIndexer != null) { @@ -77,6 +76,9 @@ namespace Umbraco.Web.Search /// static void ContentTypeCacheRefresherCacheUpdated(ContentTypeCacheRefresher sender, CacheRefresherEventArgs e) { + if (Suspendable.ExamineEvents.CanIndex == false) + return; + var indexersToUpdated = ExamineManager.Instance.IndexProviderCollection.OfType(); foreach (var provider in indexersToUpdated) { @@ -114,7 +116,7 @@ namespace Umbraco.Web.Search } } - //TODO: We need to update Examine to support re-indexing multiple items at once instead of one by one which will speed up + //TODO: We need to update Examine to support re-indexing multiple items at once instead of one by one which will speed up // the re-indexing process, we don't want to revert to rebuilding the whole thing! if (contentTypesChanged.Count > 0) @@ -129,8 +131,8 @@ namespace Umbraco.Web.Search { ReIndexForContent(contentItem, contentItem.HasPublishedVersion && contentItem.Trashed == false); } - } - } + } + } } if (mediaTypesChanged.Count > 0) { @@ -163,11 +165,14 @@ namespace Umbraco.Web.Search } } } - + } static void MemberCacheRefresherCacheUpdated(MemberCacheRefresher sender, CacheRefresherEventArgs e) { + if (Suspendable.ExamineEvents.CanIndex == false) + return; + switch (e.MessageType) { case MessageType.RefreshById: @@ -215,6 +220,9 @@ namespace Umbraco.Web.Search /// static void MediaCacheRefresherCacheUpdated(MediaCacheRefresher sender, CacheRefresherEventArgs e) { + if (Suspendable.ExamineEvents.CanIndex == false) + return; + switch (e.MessageType) { case MessageType.RefreshById: @@ -252,13 +260,13 @@ namespace Umbraco.Web.Search if (media1 != null) { ReIndexForMedia(media1, media1.Trashed == false); - } + } break; case MediaCacheRefresher.OperationType.Trashed: - + //keep if trashed for indexes supporting unpublished //(delete the index from all indexes not supporting unpublished content) - + DeleteIndexForEntity(payload.Id, true); //We then need to re-index this item for all indexes supporting unpublished content @@ -272,20 +280,20 @@ namespace Umbraco.Web.Search case MediaCacheRefresher.OperationType.Deleted: //permanently remove from all indexes - + DeleteIndexForEntity(payload.Id, false); break; default: throw new ArgumentOutOfRangeException(); - } - } + } + } } break; - case MessageType.RefreshByInstance: - case MessageType.RemoveByInstance: - case MessageType.RefreshAll: + case MessageType.RefreshByInstance: + case MessageType.RemoveByInstance: + case MessageType.RefreshAll: default: //We don't support these, these message types will not fire for media break; @@ -302,6 +310,9 @@ namespace Umbraco.Web.Search /// static void PublishedPageCacheRefresherCacheUpdated(PageCacheRefresher sender, CacheRefresherEventArgs e) { + if (Suspendable.ExamineEvents.CanIndex == false) + return; + switch (e.MessageType) { case MessageType.RefreshById: @@ -312,8 +323,8 @@ namespace Umbraco.Web.Search } break; case MessageType.RemoveById: - - //This is triggered when the item has been unpublished or trashed (which also performs an unpublish). + + //This is triggered when the item has been unpublished or trashed (which also performs an unpublish). var c2 = ApplicationContext.Current.Services.ContentService.GetById((int)e.MessageObject); if (c2 != null) @@ -368,6 +379,9 @@ namespace Umbraco.Web.Search /// static void UnpublishedPageCacheRefresherCacheUpdated(UnpublishedPageCacheRefresher sender, CacheRefresherEventArgs e) { + if (Suspendable.ExamineEvents.CanIndex == false) + return; + switch (e.MessageType) { case MessageType.RefreshById: @@ -378,9 +392,9 @@ namespace Umbraco.Web.Search } break; case MessageType.RemoveById: - + // This is triggered when the item is permanently deleted - + DeleteIndexForEntity((int)e.MessageObject, false); break; case MessageType.RefreshByInstance: @@ -399,7 +413,7 @@ namespace Umbraco.Web.Search { DeleteIndexForEntity(c4.Id, false); } - break; + break; case MessageType.RefreshByJson: var jsonPayloads = UnpublishedPageCacheRefresher.DeserializeFromJsonPayload((string)e.MessageObject); @@ -409,29 +423,28 @@ namespace Umbraco.Web.Search { switch (payload.Operation) { - case UnpublishedPageCacheRefresher.OperationType.Deleted: + case UnpublishedPageCacheRefresher.OperationType.Deleted: //permanently remove from all indexes - + DeleteIndexForEntity(payload.Id, false); break; default: throw new ArgumentOutOfRangeException(); - } - } + } + } } break; - case MessageType.RefreshAll: + case MessageType.RefreshAll: default: //We don't support these, these message types will not fire for unpublished content break; } } - private static void ReIndexForMember(IMember member) { ExamineManager.Instance.ReIndexNode( @@ -447,7 +460,7 @@ namespace Umbraco.Web.Search ///
/// /// - + private static void IndexerDocumentWriting(object sender, DocumentWritingEventArgs e) { if (e.Fields.Keys.Contains("nodeName")) @@ -463,7 +476,7 @@ namespace Umbraco.Web.Search )); } } - + private static void ReIndexForMedia(IMedia sender, bool isMediaPublished) { var xml = sender.ToXml(); @@ -497,7 +510,7 @@ namespace Umbraco.Web.Search //if keepIfUnpublished == true then only delete this item from indexes not supporting unpublished content, // otherwise if keepIfUnpublished == false then remove from all indexes - + .Where(x => keepIfUnpublished == false || x.SupportUnpublishedContent == false) .Where(x => x.EnableDefaultEventHandler)); } @@ -518,7 +531,7 @@ namespace Umbraco.Web.Search ExamineManager.Instance.ReIndexNode( xml, IndexTypes.Content, ExamineManager.Instance.IndexProviderCollection.OfType() - + //Index this item for all indexers if the content is published, otherwise if the item is not published // then only index this for indexers supporting unpublished content @@ -531,10 +544,10 @@ namespace Umbraco.Web.Search /// /// /// true if data is going to be returned from cache - /// + /// [Obsolete("This method is no longer used and will be removed from the core in future versions, the cacheOnly parameter has no effect. Use the other ToXDocument overload instead")] public static XDocument ToXDocument(Content node, bool cacheOnly) - { + { return ToXDocument(node); } @@ -542,7 +555,7 @@ namespace Umbraco.Web.Search /// Converts a content node to Xml /// /// - /// + /// private static XDocument ToXDocument(Content node) { if (TypeHelper.IsTypeAssignableFrom(node)) @@ -561,7 +574,7 @@ namespace Umbraco.Web.Search if (xNode.Attributes["nodeTypeAlias"] == null) { - //we'll add the nodeTypeAlias ourselves + //we'll add the nodeTypeAlias ourselves XmlAttribute d = xDoc.CreateAttribute("nodeTypeAlias"); d.Value = node.ContentType.Alias; xNode.Attributes.Append(d); @@ -569,6 +582,5 @@ namespace Umbraco.Web.Search return new XDocument(ExamineXmlExtensions.ToXElement(xNode)); } - } } \ No newline at end of file diff --git a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs index 69c926db3e..9953eea664 100644 --- a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs +++ b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs @@ -90,7 +90,8 @@ namespace Umbraco.Web.Security.Identity appContext.Services.UserService, appContext.Services.EntityService, appContext.Services.ExternalLoginService, - userMembershipProvider)); + userMembershipProvider, + UmbracoConfig.For.UmbracoSettings().Content)); app.SetBackOfficeUserManagerType(); @@ -119,7 +120,8 @@ namespace Umbraco.Web.Security.Identity (options, owinContext) => BackOfficeUserManager.Create( options, customUserStore, - userMembershipProvider)); + userMembershipProvider, + UmbracoConfig.For.UmbracoSettings().Content)); app.SetBackOfficeUserManagerType(); diff --git a/src/Umbraco.Web/Security/Providers/UsersMembershipProvider.cs b/src/Umbraco.Web/Security/Providers/UsersMembershipProvider.cs index 7034101a83..fdebf78480 100644 --- a/src/Umbraco.Web/Security/Providers/UsersMembershipProvider.cs +++ b/src/Umbraco.Web/Security/Providers/UsersMembershipProvider.cs @@ -42,10 +42,36 @@ namespace Umbraco.Web.Security.Providers return entity.AsConcreteMembershipUser(Name, true); } + private bool _allowManuallyChangingPassword = false; + private bool _enablePasswordReset = false; + + /// + /// Indicates whether the membership provider is configured to allow users to reset their passwords. + /// + /// + /// true if the membership provider supports password reset; otherwise, false. The default is FALSE for users. + public override bool EnablePasswordReset + { + get { return _enablePasswordReset; } + } + + /// + /// For backwards compatibility, this provider supports this option by default it is FALSE for users + /// + public override bool AllowManuallyChangingPassword + { + get { return _allowManuallyChangingPassword; } + } + public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config) { base.Initialize(name, config); + if (config == null) { throw new ArgumentNullException("config"); } + + _allowManuallyChangingPassword = config.GetValue("allowManuallyChangingPassword", false); + _enablePasswordReset = config.GetValue("enablePasswordReset", false); + // test for membertype (if not specified, choose the first member type available) // We'll support both names for legacy reasons: defaultUserTypeAlias & defaultUserGroupAlias diff --git a/src/Umbraco.Web/Suspendable.cs b/src/Umbraco.Web/Suspendable.cs new file mode 100644 index 0000000000..db4ef53485 --- /dev/null +++ b/src/Umbraco.Web/Suspendable.cs @@ -0,0 +1,116 @@ +using System; +using System.Diagnostics; +using Examine; +using Examine.Providers; +using Umbraco.Core; +using Umbraco.Web.Cache; + +namespace Umbraco.Web +{ + internal static class Suspendable + { + public static class PageCacheRefresher + { + private static bool _tried, _suspended; + + public static bool CanRefreshDocumentCacheFromDatabase + { + get + { + // trying a full refresh + if (_suspended == false) return true; + _tried = true; // remember we tried + return false; + } + } + + public static bool CanUpdateDocumentCache + { + get + { + // trying a partial update + // ok if not suspended, or if we haven't done a full already + return _suspended == false || _tried == false; + } + } + + public static void SuspendDocumentCache() + { + ApplicationContext.Current.ProfilingLogger.Logger.Info(typeof (PageCacheRefresher), "Suspend document cache."); + _suspended = true; + } + + public static void ResumeDocumentCache() + { + _suspended = false; + + ApplicationContext.Current.ProfilingLogger.Logger.Info(typeof (PageCacheRefresher), string.Format("Resume document cache (reload:{0}).", _tried ? "true" : "false")); + + if (_tried == false) return; + _tried = false; + + var pageRefresher = CacheRefreshersResolver.Current.GetById(new Guid(DistributedCache.PageCacheRefresherId)); + pageRefresher.RefreshAll(); + } + } + + public static class ExamineEvents + { + private static bool _tried, _suspended; + + public static bool CanIndex + { + get + { + if (_suspended == false) return true; + _tried = true; // remember we tried + return false; + } + } + + public static void SuspendIndexers() + { + ApplicationContext.Current.ProfilingLogger.Logger.Info(typeof (ExamineEvents), "Suspend indexers."); + _suspended = true; + } + + public static void ResumeIndexers() + { + _suspended = false; + + ApplicationContext.Current.ProfilingLogger.Logger.Info(typeof (ExamineEvents), string.Format("Resume indexers (rebuild:{0}).", _tried ? "true" : "false")); + + if (_tried == false) return; + _tried = false; + + // fixme - could we fork this on a background thread? + foreach (BaseIndexProvider indexer in ExamineManager.Instance.IndexProviderCollection) + { + indexer.RebuildIndex(); + } + } + } + + public static class ScheduledPublishing + { + private static bool _suspended; + + public static bool CanRun + { + get { return _suspended == false; } + } + + public static void Suspend() + { + ApplicationContext.Current.ProfilingLogger.Logger.Info(typeof (ScheduledPublishing), "Suspend scheduled publishing."); + _suspended = true; + } + + public static void Resume() + { + ApplicationContext.Current.ProfilingLogger.Logger.Info(typeof (ScheduledPublishing), "Resume scheduled publishing."); + _suspended = false; + } + } + } +} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index fb628ad137..e5b2568430 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -491,6 +491,7 @@ + diff --git a/src/Umbraco.Web/umbraco.presentation/content.cs b/src/Umbraco.Web/umbraco.presentation/content.cs index 54bb1ac2f1..a7d40fa088 100644 --- a/src/Umbraco.Web/umbraco.presentation/content.cs +++ b/src/Umbraco.Web/umbraco.presentation/content.cs @@ -2,6 +2,7 @@ using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics; using System.Globalization; using System.IO; using System.Text; diff --git a/src/Umbraco.Web/umbraco.presentation/library.cs b/src/Umbraco.Web/umbraco.presentation/library.cs index 4ab89c5c7d..651cf9a9f4 100644 --- a/src/Umbraco.Web/umbraco.presentation/library.cs +++ b/src/Umbraco.Web/umbraco.presentation/library.cs @@ -1613,7 +1613,8 @@ namespace umbraco public static void SendMail(string fromMail, string toMail, string subject, string body, bool isHtml) { try - { + { + var mailSender = new EmailSender(); using (var mail = new MailMessage()) { mail.From = new MailAddress(fromMail.Trim()); @@ -1622,8 +1623,7 @@ namespace umbraco mail.Subject = subject; mail.IsBodyHtml = isHtml; mail.Body = body; - using (var smtpClient = new SmtpClient()) - smtpClient.Send(mail); + mailSender.Send(mail); } } catch (Exception ee) diff --git a/src/umbraco.cms/businesslogic/translation/Translation.cs b/src/umbraco.cms/businesslogic/translation/Translation.cs index 1cddf4e8e9..9c31cdb47d 100644 --- a/src/umbraco.cms/businesslogic/translation/Translation.cs +++ b/src/umbraco.cms/businesslogic/translation/Translation.cs @@ -59,6 +59,7 @@ namespace umbraco.cms.businesslogic.translation { try { + var mailSender = new EmailSender(); using (var mail = new MailMessage()) { mail.From = new MailAddress(user.Email.Trim()); @@ -66,8 +67,7 @@ namespace umbraco.cms.businesslogic.translation mail.Subject = ui.Text("translation", "mailSubject", subjectVars, translator); ; mail.IsBodyHtml = false; mail.Body = ui.Text("translation", "mailBody", bodyVars, translator); ; - using (var smtpClient = new SmtpClient()) - smtpClient.Send(mail); + mailSender.Send(mail); } } catch (Exception ex)