diff --git a/.gitignore b/.gitignore
index f713fa4079..808b964260 100644
--- a/.gitignore
+++ b/.gitignore
@@ -64,6 +64,7 @@ src/Umbraco.Web.UI/[Ww]eb.config
*.transformed
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/src/SolutionInfo.cs b/src/SolutionInfo.cs
index e8a36f0da7..bf5ba3392f 100644
--- a/src/SolutionInfo.cs
+++ b/src/SolutionInfo.cs
@@ -11,5 +11,5 @@ using System.Resources;
[assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyFileVersion("7.6.5")]
-[assembly: AssemblyInformationalVersion("7.6.5")]
\ No newline at end of file
+[assembly: AssemblyFileVersion("7.6.7")]
+[assembly: AssemblyInformationalVersion("7.6.7")]
\ No newline at end of file
diff --git a/src/Umbraco.Core/Configuration/UmbracoVersion.cs b/src/Umbraco.Core/Configuration/UmbracoVersion.cs
index 0d093c395e..19b1c2c405 100644
--- a/src/Umbraco.Core/Configuration/UmbracoVersion.cs
+++ b/src/Umbraco.Core/Configuration/UmbracoVersion.cs
@@ -6,7 +6,7 @@ namespace Umbraco.Core.Configuration
{
public class UmbracoVersion
{
- private static readonly Version Version = new Version("7.6.5");
+ private static readonly Version Version = new Version("7.6.7");
///
/// Gets the current version of Umbraco.
diff --git a/src/Umbraco.Core/Constants-ObjectTypes.cs b/src/Umbraco.Core/Constants-ObjectTypes.cs
index 4a79437c61..a1e6e55ee9 100644
--- a/src/Umbraco.Core/Constants-ObjectTypes.cs
+++ b/src/Umbraco.Core/Constants-ObjectTypes.cs
@@ -222,6 +222,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/Constants-Security.cs b/src/Umbraco.Core/Constants-Security.cs
index bd2e1c5acf..e4f2e5b4a8 100644
--- a/src/Umbraco.Core/Constants-Security.cs
+++ b/src/Umbraco.Core/Constants-Security.cs
@@ -14,6 +14,8 @@ namespace Umbraco.Core
public const string BackOfficeTokenAuthenticationType = "UmbracoBackOfficeToken";
public const string BackOfficeTwoFactorAuthenticationType = "UmbracoTwoFactorCookie";
+ internal const string EmptyPasswordPrefix = "___UIDEMPTYPWORD__";
+
///
/// The prefix used for external identity providers for their authentication type
///
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/EnumerableExtensions.cs b/src/Umbraco.Core/EnumerableExtensions.cs
index e8565f3bc7..bb115b394d 100644
--- a/src/Umbraco.Core/EnumerableExtensions.cs
+++ b/src/Umbraco.Core/EnumerableExtensions.cs
@@ -295,5 +295,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/CancellableEventArgs.cs b/src/Umbraco.Core/Events/CancellableEventArgs.cs
index a102ea66ef..7a10623427 100644
--- a/src/Umbraco.Core/Events/CancellableEventArgs.cs
+++ b/src/Umbraco.Core/Events/CancellableEventArgs.cs
@@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Security.Permissions;
-using Umbraco.Core.Models.PublishedContent;
namespace Umbraco.Core.Events
{
@@ -13,6 +12,9 @@ namespace Umbraco.Core.Events
public class CancellableEventArgs : EventArgs, IEquatable
{
private bool _cancel;
+ private Dictionary _eventState;
+
+ private static readonly ReadOnlyDictionary EmptyAdditionalData = new ReadOnlyDictionary(new Dictionary());
public CancellableEventArgs(bool canCancel, EventMessages messages, IDictionary additionalData)
{
@@ -26,7 +28,7 @@ namespace Umbraco.Core.Events
if (eventMessages == null) throw new ArgumentNullException("eventMessages");
CanCancel = canCancel;
Messages = eventMessages;
- AdditionalData = new ReadOnlyDictionary(new Dictionary());
+ AdditionalData = EmptyAdditionalData;
}
public CancellableEventArgs(bool canCancel)
@@ -34,18 +36,16 @@ namespace Umbraco.Core.Events
CanCancel = canCancel;
//create a standalone messages
Messages = new EventMessages();
- AdditionalData = new ReadOnlyDictionary(new Dictionary());
+ AdditionalData = EmptyAdditionalData;
}
public CancellableEventArgs(EventMessages eventMessages)
: this(true, eventMessages)
- {
- }
+ { }
public CancellableEventArgs()
: this(true)
- {
- }
+ { }
///
/// Flag to determine if this instance will support being cancellable
@@ -95,10 +95,19 @@ namespace Umbraco.Core.Events
/// In some cases raised evens might need to contain additional arbitrary readonly data which can be read by event subscribers
///
///
- /// This allows for a bit of flexibility in our event raising - it's not pretty but we need to maintain backwards compatibility
+ /// This allows for a bit of flexibility in our event raising - it's not pretty but we need to maintain backwards compatibility
/// so we cannot change the strongly typed nature for some events.
///
public ReadOnlyDictionary AdditionalData { get; private set; }
+
+ ///
+ /// This can be used by event subscribers to store state in the event args so they easily deal with custom state data between a starting ("ing")
+ /// event and an ending ("ed") event
+ ///
+ public IDictionary EventState
+ {
+ get { return _eventState ?? (_eventState = new Dictionary()); }
+ }
public bool Equals(CancellableEventArgs other)
{
@@ -111,13 +120,13 @@ namespace Umbraco.Core.Events
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
- if (obj.GetType() != this.GetType()) return false;
+ if (obj.GetType() != GetType()) return false;
return Equals((CancellableEventArgs) obj);
}
public override int GetHashCode()
{
- return (AdditionalData != null ? AdditionalData.GetHashCode() : 0);
+ return AdditionalData != null ? AdditionalData.GetHashCode() : 0;
}
public static bool operator ==(CancellableEventArgs left, CancellableEventArgs right)
@@ -127,7 +136,7 @@ namespace Umbraco.Core.Events
public static bool operator !=(CancellableEventArgs left, CancellableEventArgs right)
{
- return !Equals(left, right);
+ return Equals(left, right) == false;
}
}
}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Events/DeleteEventArgs.cs b/src/Umbraco.Core/Events/DeleteEventArgs.cs
index d0a4f024e1..188ce70bd1 100644
--- a/src/Umbraco.Core/Events/DeleteEventArgs.cs
+++ b/src/Umbraco.Core/Events/DeleteEventArgs.cs
@@ -99,7 +99,8 @@ namespace Umbraco.Core.Events
///
public IEnumerable DeletedEntities
{
- get { return EventObject; }
+ get { return EventObject; }
+ internal set { EventObject = value; }
}
///
diff --git a/src/Umbraco.Core/Events/MoveEventArgs.cs b/src/Umbraco.Core/Events/MoveEventArgs.cs
index 228e1ca2f7..18c02ff46a 100644
--- a/src/Umbraco.Core/Events/MoveEventArgs.cs
+++ b/src/Umbraco.Core/Events/MoveEventArgs.cs
@@ -6,6 +6,8 @@ namespace Umbraco.Core.Events
{
public class MoveEventArgs : CancellableObjectEventArgs, IEquatable>
{
+ private IEnumerable> _moveInfoCollection;
+
///
/// Constructor accepting a collection of MoveEventInfo objects
///
@@ -107,7 +109,24 @@ namespace Umbraco.Core.Events
///
/// Gets all MoveEventInfo objects used to create the object
///
- public IEnumerable> MoveInfoCollection { get; private set; }
+ public IEnumerable> MoveInfoCollection
+ {
+ get { return _moveInfoCollection; }
+ set
+ {
+ var first = value.FirstOrDefault();
+ if (first == null)
+ {
+ throw new InvalidOperationException("MoveInfoCollection must have at least one item");
+ }
+
+ _moveInfoCollection = value;
+
+ //assign the legacy props
+ EventObject = first.Entity;
+ ParentId = first.NewParentId;
+ }
+ }
///
/// The entity being moved
diff --git a/src/Umbraco.Core/Events/RecycleBinEventArgs.cs b/src/Umbraco.Core/Events/RecycleBinEventArgs.cs
index c6d7c659b7..62c00d7c5b 100644
--- a/src/Umbraco.Core/Events/RecycleBinEventArgs.cs
+++ b/src/Umbraco.Core/Events/RecycleBinEventArgs.cs
@@ -107,7 +107,7 @@ namespace Umbraco.Core.Events
///
/// Boolean indicating whether the Recycle Bin was emptied successfully
///
- public bool RecycleBinEmptiedSuccessfully { get; private set; }
+ public bool RecycleBinEmptiedSuccessfully { get; set; }
///
/// Boolean indicating whether this event was fired for the Content's Recycle Bin.
diff --git a/src/Umbraco.Core/HashCodeCombiner.cs b/src/Umbraco.Core/HashCodeCombiner.cs
index d3a55d5256..5f99c65787 100644
--- a/src/Umbraco.Core/HashCodeCombiner.cs
+++ b/src/Umbraco.Core/HashCodeCombiner.cs
@@ -2,17 +2,19 @@
using System.Globalization;
using System.IO;
using System.Linq;
-using System.Text;
namespace Umbraco.Core
{
///
- /// Used to create a hash code from multiple objects.
+ /// Used to create a .NET HashCode from multiple objects.
///
///
/// .Net has a class the same as this: System.Web.Util.HashCodeCombiner and of course it works for all sorts of things
/// which we've not included here as we just need a quick easy class for this in order to create a unique
/// hash of directories/files to see if they have changed.
+ ///
+ /// NOTE: It's probably best to not relying on the hashing result across AppDomains! If you need a constant/reliable hash value
+ /// between AppDomains use SHA1. This is perfect for hashing things in a very fast way for a single AppDomain.
///
internal class HashCodeCombiner
{
diff --git a/src/Umbraco.Core/HashGenerator.cs b/src/Umbraco.Core/HashGenerator.cs
new file mode 100644
index 0000000000..7306dc9045
--- /dev/null
+++ b/src/Umbraco.Core/HashGenerator.cs
@@ -0,0 +1,154 @@
+using System;
+using System.Globalization;
+using System.IO;
+using System.Security.Cryptography;
+using System.Text;
+
+namespace Umbraco.Core
+{
+ ///
+ /// Used to generate a string hash using crypto libraries over multiple objects
+ ///
+ ///
+ /// This should be used to generate a reliable hash that survives AppDomain restarts.
+ /// This will use the crypto libs to generate the hash and will try to ensure that
+ /// strings, etc... are not re-allocated so it's not consuming much memory.
+ ///
+ internal class HashGenerator : DisposableObject
+ {
+ public HashGenerator()
+ {
+ _writer = new StreamWriter(_ms, Encoding.Unicode, 1024, leaveOpen: true);
+ }
+
+ private readonly MemoryStream _ms = new MemoryStream();
+ private StreamWriter _writer;
+
+ internal void AddInt(int i)
+ {
+ _writer.Write(i);
+ }
+
+ internal void AddLong(long i)
+ {
+ _writer.Write(i);
+ }
+
+ internal void AddObject(object o)
+ {
+ _writer.Write(o);
+ }
+
+ internal void AddDateTime(DateTime d)
+ {
+ _writer.Write(d.Ticks);;
+ }
+
+ internal void AddString(string s)
+ {
+ if (s != null)
+ _writer.Write(s);
+ }
+
+ internal void AddCaseInsensitiveString(string s)
+ {
+ //I've tried to no allocate a new string with this which can be done if we use the CompareInfo.GetSortKey method which will create a new
+ //byte array that we can use to write to the output, however this also allocates new objects so i really don't think the performance
+ //would be much different. In any case, i'll leave this here for reference. We could write the bytes out based on the sort key,
+ //this is how we could deal with case insensitivity without allocating another string
+ //for reference see: https://stackoverflow.com/a/10452967/694494
+ //we could go a step further and s.Normalize() but we're not really dealing with crazy unicode with this class so far.
+
+ if (s != null)
+ _writer.Write(s.ToUpperInvariant());
+ }
+
+ internal void AddFileSystemItem(FileSystemInfo f)
+ {
+ //if it doesn't exist, don't proceed.
+ if (f.Exists == false)
+ return;
+
+ AddCaseInsensitiveString(f.FullName);
+ AddDateTime(f.CreationTimeUtc);
+ AddDateTime(f.LastWriteTimeUtc);
+
+ //check if it is a file or folder
+ var fileInfo = f as FileInfo;
+ if (fileInfo != null)
+ {
+ AddLong(fileInfo.Length);
+ }
+
+ var dirInfo = f as DirectoryInfo;
+ if (dirInfo != null)
+ {
+ foreach (var d in dirInfo.GetFiles())
+ {
+ AddFile(d);
+ }
+ foreach (var s in dirInfo.GetDirectories())
+ {
+ AddFolder(s);
+ }
+ }
+ }
+
+ internal void AddFile(FileInfo f)
+ {
+ AddFileSystemItem(f);
+ }
+
+ internal void AddFolder(DirectoryInfo d)
+ {
+ AddFileSystemItem(d);
+ }
+
+ ///
+ /// Returns the generated hash output of all added objects
+ ///
+ ///
+ internal string GenerateHash()
+ {
+ //flush,close,dispose the writer,then create a new one since it's possible to keep adding after GenerateHash is called.
+
+ _writer.Flush();
+ _writer.Close();
+ _writer.Dispose();
+ _writer = new StreamWriter(_ms, Encoding.UTF8, 1024, leaveOpen: true);
+
+ var hashType = CryptoConfig.AllowOnlyFipsAlgorithms ? "SHA1" : "MD5";
+
+ //create an instance of the correct hashing provider based on the type passed in
+ var hasher = HashAlgorithm.Create(hashType);
+ if (hasher == null) throw new InvalidOperationException("No hashing type found by name " + hashType);
+ using (hasher)
+ {
+ var buffer = _ms.GetBuffer();
+ //get the hashed values created by our selected provider
+ var hashedByteArray = hasher.ComputeHash(buffer);
+
+ //create a StringBuilder object
+ var stringBuilder = new StringBuilder();
+
+ //loop to each each byte
+ foreach (var b in hashedByteArray)
+ {
+ //append it to our StringBuilder
+ stringBuilder.Append(b.ToString("x2"));
+ }
+
+ //return the hashed value
+ return stringBuilder.ToString();
+ }
+ }
+
+ protected override void DisposeResources()
+ {
+ _writer.Close();
+ _writer.Dispose();
+ _ms.Close();
+ _ms.Dispose();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Models/ContentBase.cs b/src/Umbraco.Core/Models/ContentBase.cs
index 0fc3bac044..32eec7f284 100644
--- a/src/Umbraco.Core/Models/ContentBase.cs
+++ b/src/Umbraco.Core/Models/ContentBase.cs
@@ -264,7 +264,7 @@ namespace Umbraco.Core.Models
/// Value as an
public virtual object GetValue(string propertyTypeAlias)
{
- return Properties[propertyTypeAlias].Value;
+ return Properties.Contains(propertyTypeAlias) ? Properties[propertyTypeAlias].Value : null;
}
///
@@ -275,8 +275,13 @@ namespace Umbraco.Core.Models
/// Value as a
public virtual TPassType GetValue(string propertyTypeAlias)
{
+ if (Properties.Contains(propertyTypeAlias) == false)
+ {
+ return default(TPassType);
+ }
+
var convertAttempt = Properties[propertyTypeAlias].Value.TryConvertTo();
- return convertAttempt.Success ? convertAttempt.Result : default(TPassType);
+ return convertAttempt.Success ? convertAttempt.Result : default(TPassType);
}
///
diff --git a/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs b/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs
index fab34e5f17..78b58fc0d6 100644
--- a/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs
+++ b/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs
@@ -42,7 +42,7 @@ namespace Umbraco.Core.Models.Identity
private string GetPasswordHash(string storedPass)
{
- return storedPass.StartsWith("___UIDEMPTYPWORD__") ? null : storedPass;
+ return storedPass.StartsWith(Constants.Security.EmptyPasswordPrefix) ? null : storedPass;
}
}
}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Models/UmbracoObjectTypes.cs b/src/Umbraco.Core/Models/UmbracoObjectTypes.cs
index d0136a10a4..d4caaef768 100644
--- a/src/Umbraco.Core/Models/UmbracoObjectTypes.cs
+++ b/src/Umbraco.Core/Models/UmbracoObjectTypes.cs
@@ -185,6 +185,13 @@ namespace Umbraco.Core.Models
///
[UmbracoObjectType(Constants.ObjectTypes.Language)]
[FriendlyName("Language")]
- Language
+ Language,
+
+ ///
+ /// Reserved Identifier
+ ///
+ [UmbracoObjectType(Constants.ObjectTypes.IdReservation)]
+ [FriendlyName("Identifier Reservation")]
+ IdReservation
}
}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Models/UserExtensions.cs b/src/Umbraco.Core/Models/UserExtensions.cs
index ece63b4889..a6f388addc 100644
--- a/src/Umbraco.Core/Models/UserExtensions.cs
+++ b/src/Umbraco.Core/Models/UserExtensions.cs
@@ -1,5 +1,6 @@
using System;
using System.Globalization;
+using Umbraco.Core.Configuration;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Services;
@@ -12,7 +13,7 @@ namespace Umbraco.Core.Models
///
///
///
- ///
+ ///
public static CultureInfo GetUserCulture(this IUser user, ILocalizedTextService textService)
{
if (user == null) throw new ArgumentNullException("user");
@@ -34,7 +35,7 @@ namespace Umbraco.Core.Models
catch (CultureNotFoundException)
{
//return the default one
- return CultureInfo.GetCultureInfo("en");
+ return CultureInfo.GetCultureInfo(GlobalSettings.DefaultUILanguage);
}
}
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/Migrations/Upgrades/TargetVersionSevenTwoZero/AddIndexToUmbracoNodeTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenTwoZero/AddIndexToUmbracoNodeTable.cs
deleted file mode 100644
index 7d5109a1db..0000000000
--- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenTwoZero/AddIndexToUmbracoNodeTable.cs
+++ /dev/null
@@ -1,46 +0,0 @@
-using System.Linq;
-using Umbraco.Core.Configuration;
-using Umbraco.Core.Logging;
-using Umbraco.Core.Persistence.DatabaseModelDefinitions;
-using Umbraco.Core.Persistence.SqlSyntax;
-
-namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenTwoZero
-{
- [Migration("7.2.0", 3, Constants.System.UmbracoMigrationName)]
- public class AddIndexToUmbracoNodeTable : MigrationBase
- {
- private readonly bool _skipIndexCheck;
-
- internal AddIndexToUmbracoNodeTable(ISqlSyntaxProvider sqlSyntax, ILogger logger, bool skipIndexCheck) : base(sqlSyntax, logger)
- {
- _skipIndexCheck = skipIndexCheck;
- }
-
- public AddIndexToUmbracoNodeTable(ISqlSyntaxProvider sqlSyntax, ILogger logger) : base(sqlSyntax, logger)
- {
- }
-
- public override void Up()
- {
- var dbIndexes = _skipIndexCheck ? new DbIndexDefinition[] { } : SqlSyntax.GetDefinedIndexes(Context.Database)
- .Select(x => new DbIndexDefinition
- {
- TableName = x.Item1,
- IndexName = x.Item2,
- ColumnName = x.Item3,
- IsUnique = x.Item4
- }).ToArray();
-
- //make sure it doesn't already exist
- if (dbIndexes.Any(x => x.IndexName.InvariantEquals("IX_umbracoNodeUniqueID")) == false)
- {
- Create.Index("IX_umbracoNodeUniqueID").OnTable("umbracoNode").OnColumn("uniqueID").Ascending().WithOptions().NonClustered();
- }
- }
-
- public override void Down()
- {
- Delete.Index("IX_umbracoNodeUniqueID").OnTable("umbracoNode");
- }
- }
-}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs
index f3d199e175..a01cbaa2e8 100644
--- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs
@@ -435,7 +435,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);
@@ -1122,31 +1139,10 @@ ORDER BY cmsContentVersion.id DESC
if (EnsureUniqueNaming == false)
return nodeName;
- var sql = new Sql();
- sql.Select("*")
- .From()
- .Where(x => x.NodeObjectType == NodeObjectTypeId && x.ParentId == parentId && x.Text.StartsWith(nodeName));
+ var names = Database.Fetch("SELECT id, text AS name FROM umbracoNode WHERE nodeObjectType=@objectType AND parentId=@parentId",
+ new { objectType = NodeObjectTypeId, parentId });
- int uniqueNumber = 1;
- var currentName = nodeName;
-
- var dtos = Database.Fetch(sql);
- if (dtos.Any())
- {
- var results = dtos.OrderBy(x => x.Text, new SimilarNodeNameComparer());
- foreach (var dto in results)
- {
- if (id != 0 && id == dto.NodeId) continue;
-
- if (dto.Text.ToLowerInvariant().Equals(currentName.ToLowerInvariant()))
- {
- currentName = nodeName + string.Format(" ({0})", uniqueNumber);
- uniqueNumber++;
- }
- }
- }
-
- return currentName;
+ return SimilarNodeName.GetUniqueName(names, id, nodeName);
}
///
diff --git a/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs b/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs
index 1690b36148..eb64ea190b 100644
--- a/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs
@@ -450,33 +450,10 @@ AND umbracoNode.id <> @id",
private string EnsureUniqueNodeName(string nodeName, int id = 0)
{
+ var names = Database.Fetch("SELECT id, text AS name FROM umbracoNode WHERE nodeObjectType=@objectType",
+ new { objectType = NodeObjectTypeId });
-
- var sql = new Sql();
- sql.Select("*")
- .From(SqlSyntax)
- .Where(x => x.NodeObjectType == NodeObjectTypeId && x.Text.StartsWith(nodeName));
-
- int uniqueNumber = 1;
- var currentName = nodeName;
-
- var dtos = Database.Fetch(sql);
- if (dtos.Any())
- {
- var results = dtos.OrderBy(x => x.Text, new SimilarNodeNameComparer());
- foreach (var dto in results)
- {
- if (id != 0 && id == dto.NodeId) continue;
-
- if (dto.Text.ToLowerInvariant().Equals(currentName.ToLowerInvariant()))
- {
- currentName = nodeName + string.Format(" ({0})", uniqueNumber);
- uniqueNumber++;
- }
- }
- }
-
- return currentName;
+ return SimilarNodeName.GetUniqueName(names, id, nodeName);
}
///
diff --git a/src/Umbraco.Core/Persistence/Repositories/DictionaryRepository.cs b/src/Umbraco.Core/Persistence/Repositories/DictionaryRepository.cs
index 968c2d9cb0..7196165e6f 100644
--- a/src/Umbraco.Core/Persistence/Repositories/DictionaryRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/DictionaryRepository.cs
@@ -260,6 +260,19 @@ namespace Umbraco.Core.Persistence.Repositories
return GetByQuery(query);
}
+ public Dictionary GetDictionaryItemKeyMap()
+ {
+ var columns = new[] { "key", "id" }.Select(x => (object) SqlSyntax.GetQuotedColumnName(x)).ToArray();
+ var sql = new Sql().Select(columns).From(SqlSyntax);
+ return Database.Fetch(sql).ToDictionary(x => x.Key, x => x.Id);
+ }
+
+ private class DictionaryItemKeyIdDto
+ {
+ public string Key { get; set; }
+ public Guid Id { get; set; }
+ }
+
public IEnumerable GetDictionaryItemDescendants(Guid? parentId)
{
//This methods will look up children at each level, since we do not store a path for dictionary (ATM), we need to do a recursive
diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IDictionaryRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IDictionaryRepository.cs
index d030bcda2a..5c120c76cb 100644
--- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IDictionaryRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IDictionaryRepository.cs
@@ -9,5 +9,6 @@ namespace Umbraco.Core.Persistence.Repositories
IDictionaryItem Get(Guid uniqueId);
IDictionaryItem Get(string key);
IEnumerable GetDictionaryItemDescendants(Guid? parentId);
+ Dictionary GetDictionaryItemKeyMap();
}
}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs
index c9f06c6c75..b22e8981b9 100644
--- a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs
@@ -84,7 +84,7 @@ namespace Umbraco.Core.Persistence.Repositories
#endregion
#region Overrides of PetaPocoRepositoryBase
-
+
protected override Sql GetBaseQuery(BaseQueryType queryType)
{
var sql = new Sql();
@@ -153,7 +153,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
@@ -164,7 +164,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();
@@ -180,7 +180,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)
{
@@ -303,7 +303,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)
@@ -314,7 +314,7 @@ namespace Umbraco.Core.Persistence.Repositories
}
xmlIdsQuery.Where(dto => dto.NodeObjectType == mediaObjectType, SqlSyntax);
-
+
var allXmlIds = Database.Fetch(xmlIdsQuery);
var toRemove = allXmlIds.Except(allMediaIds).ToArray();
@@ -380,7 +380,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);
@@ -564,7 +581,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);
@@ -584,31 +601,10 @@ namespace Umbraco.Core.Persistence.Repositories
if (EnsureUniqueNaming == false)
return nodeName;
- var sql = new Sql();
- sql.Select("*")
- .From()
- .Where(x => x.NodeObjectType == NodeObjectTypeId && x.ParentId == parentId && x.Text.StartsWith(nodeName));
+ var names = Database.Fetch("SELECT id, text AS name FROM umbracoNode WHERE nodeObjectType=@objectType AND parentId=@parentId",
+ new { objectType = NodeObjectTypeId, parentId });
- int uniqueNumber = 1;
- var currentName = nodeName;
-
- var dtos = Database.Fetch(sql);
- if (dtos.Any())
- {
- var results = dtos.OrderBy(x => x.Text, new SimilarNodeNameComparer());
- foreach (var dto in results)
- {
- if (id != 0 && id == dto.NodeId) continue;
-
- if (dto.Text.ToLowerInvariant().Equals(currentName.ToLowerInvariant()))
- {
- currentName = nodeName + string.Format(" ({0})", uniqueNumber);
- uniqueNumber++;
- }
- }
- }
-
- return currentName;
+ return SimilarNodeName.GetUniqueName(names, id, nodeName);
}
///
diff --git a/src/Umbraco.Core/Persistence/Repositories/MemberGroupRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MemberGroupRepository.cs
index e9438b56f6..a358de08d2 100644
--- a/src/Umbraco.Core/Persistence/Repositories/MemberGroupRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/MemberGroupRepository.cs
@@ -184,30 +184,16 @@ namespace Umbraco.Core.Persistence.Repositories
public IEnumerable GetMemberGroupsForMember(string username)
{
- //find the member by username
- var memberSql = new Sql();
- var memberObjectType = new Guid(Constants.ObjectTypes.Member);
+ var sql = new Sql()
+ .Select("un.*")
+ .From("umbracoNode AS un")
+ .InnerJoin("cmsMember2MemberGroup")
+ .On("un.id = cmsMember2MemberGroup.MemberGroup")
+ .LeftJoin("(SELECT umbracoNode.id, cmsMember.LoginName FROM umbracoNode INNER JOIN cmsMember ON umbracoNode.id = cmsMember.nodeId) AS member")
+ .On("member.id = cmsMember2MemberGroup.Member")
+ .Where("un.nodeObjectType=@objectType", new {objectType = NodeObjectTypeId })
+ .Where("member.LoginName=@loginName", new {loginName = username});
- memberSql.Select("umbracoNode.id")
- .From()
- .InnerJoin()
- .On(dto => dto.NodeId, dto => dto.NodeId)
- .Where(x => x.NodeObjectType == memberObjectType)
- .Where(x => x.LoginName == username);
- var memberIdUsername = Database.Fetch(memberSql).FirstOrDefault();
- if (memberIdUsername.HasValue == false)
- {
- return Enumerable.Empty();
- }
-
- var sql = new Sql();
- sql.Select("umbracoNode.*")
- .From()
- .InnerJoin()
- .On(dto => dto.NodeId, dto => dto.MemberGroup)
- .Where(x => x.NodeObjectType == NodeObjectTypeId)
- .Where(x => x.Member == memberIdUsername.Value);
-
return Database.Fetch(sql)
.DistinctBy(dto => dto.NodeId)
.Select(x => _modelFactory.BuildEntity(x));
diff --git a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs
index 2ef795282b..74c8b45621 100644
--- a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs
@@ -4,6 +4,7 @@ using System.Globalization;
using System.Linq;
using System.Text;
using System.Xml.Linq;
+using Microsoft.AspNet.Identity;
using Umbraco.Core.Configuration.UmbracoSettings;
using Umbraco.Core.Logging;
using Umbraco.Core.Models.EntityBase;
@@ -230,7 +231,19 @@ namespace Umbraco.Core.Persistence.Repositories
Database.Insert(dto.ContentVersionDto);
//Create the first entry in cmsMember
- dto.NodeId = nodeDto.NodeId;
+ dto.NodeId = nodeDto.NodeId;
+
+ //if the password is empty, generate one with the special prefix
+ //this will hash the guid with a salt so should be nicely random
+ if (entity.RawPasswordValue.IsNullOrWhiteSpace())
+ {
+ var aspHasher = new PasswordHasher();
+ dto.Password = Constants.Security.EmptyPasswordPrefix +
+ aspHasher.HashPassword(Guid.NewGuid().ToString("N"));
+ //re-assign
+ entity.RawPasswordValue = dto.Password;
+ }
+
Database.Insert(dto);
//Create the PropertyData for this version - cmsPropertyData
diff --git a/src/Umbraco.Core/Persistence/Repositories/SimilarNodeName.cs b/src/Umbraco.Core/Persistence/Repositories/SimilarNodeName.cs
new file mode 100644
index 0000000000..371f73b27f
--- /dev/null
+++ b/src/Umbraco.Core/Persistence/Repositories/SimilarNodeName.cs
@@ -0,0 +1,112 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Umbraco.Core.Persistence.Repositories
+{
+ internal class SimilarNodeName
+ {
+ private int _numPos = -2;
+
+ public int Id { get; set; }
+ public string Name { get; set; }
+
+ // cached - reused
+ public int NumPos
+ {
+ get
+ {
+ if (_numPos != -2) return _numPos;
+
+ var name = Name;
+
+ if (name[name.Length - 1] != ')')
+ return _numPos = -1;
+
+ var pos = name.LastIndexOf('(');
+ if (pos < 2 || pos == name.Length - 2) // < 2 and not < 0, because we want at least "x ("
+ return _numPos = -1;
+
+ return _numPos = pos;
+ }
+ }
+
+ // not cached - used only once
+ public int NumVal
+ {
+ get
+ {
+ if (NumPos < 0)
+ throw new InvalidOperationException();
+ int num;
+ if (int.TryParse(Name.Substring(NumPos + 1, Name.Length - 2 - NumPos), out num))
+ return num;
+ return 0;
+ }
+ }
+
+ // compare without allocating, nor parsing integers
+ internal class Comparer : IComparer
+ {
+ public int Compare(SimilarNodeName x, SimilarNodeName y)
+ {
+ if (x == null) throw new ArgumentNullException("x");
+ if (y == null) throw new ArgumentNullException("y");
+
+ var xpos = x.NumPos;
+ var ypos = y.NumPos;
+
+ var xname = x.Name;
+ var yname = y.Name;
+
+ if (xpos < 0 || ypos < 0 || xpos != ypos)
+ return string.Compare(xname, yname, StringComparison.Ordinal);
+
+ // compare the part before (number)
+ var n = string.Compare(xname, 0, yname, 0, xpos, StringComparison.Ordinal);
+ if (n != 0)
+ return n;
+
+ // compare (number) lengths
+ var diff = xname.Length - yname.Length;
+ if (diff != 0) return diff < 0 ? -1 : +1;
+
+ // actually compare (number)
+ var i = xpos;
+ while (i < xname.Length - 1)
+ {
+ if (xname[i] != yname[i])
+ return xname[i] < yname[i] ? -1 : +1;
+ i++;
+ }
+ return 0;
+ }
+ }
+
+ // gets a unique name
+ public static string GetUniqueName(IEnumerable names, int nodeId, string nodeName)
+ {
+ var uniqueNumber = 1;
+ var uniqueing = false;
+ foreach (var name in names.OrderBy(x => x, new Comparer()))
+ {
+ // ignore self
+ if (nodeId != 0 && name.Id == nodeId) continue;
+
+ if (uniqueing)
+ {
+ if (name.NumPos > 0 && name.Name.StartsWith(nodeName) && name.NumVal == uniqueNumber)
+ uniqueNumber++;
+ else
+ break;
+ }
+ else if (name.Name.InvariantEquals(nodeName))
+ {
+ uniqueing = true;
+ }
+ }
+
+ return uniqueing ? string.Concat(nodeName, " (", uniqueNumber.ToString(), ")") : nodeName;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Persistence/Repositories/SimilarNodeNameComparer.cs b/src/Umbraco.Core/Persistence/Repositories/SimilarNodeNameComparer.cs
deleted file mode 100644
index ce25486422..0000000000
--- a/src/Umbraco.Core/Persistence/Repositories/SimilarNodeNameComparer.cs
+++ /dev/null
@@ -1,45 +0,0 @@
-using System;
-using System.Collections.Generic;
-
-namespace Umbraco.Core.Persistence.Repositories
-{
- ///
- /// Comparer that takes into account the duplicate index of a node name
- /// This is needed as a normal alphabetic sort would go Page (1), Page (10), Page (2) etc.
- ///
- internal class SimilarNodeNameComparer : IComparer
- {
- public int Compare(string x, string y)
- {
- if (x.LastIndexOf('(') != -1 && x.LastIndexOf(')') == x.Length - 1 && y.LastIndexOf(')') == y.Length - 1)
- {
- if (x.ToLower().Substring(0, x.LastIndexOf('(')) == y.ToLower().Substring(0, y.LastIndexOf('(')))
- {
- int xDuplicateIndex = ExtractDuplicateIndex(x);
- int yDuplicateIndex = ExtractDuplicateIndex(y);
-
- if (xDuplicateIndex != 0 && yDuplicateIndex != 0)
- {
- return xDuplicateIndex.CompareTo(yDuplicateIndex);
- }
- }
- }
- return String.Compare(x.ToLower(), y.ToLower(), StringComparison.Ordinal);
- }
-
- private int ExtractDuplicateIndex(string text)
- {
- int index = 0;
-
- if (text.LastIndexOf('(') != -1 && text.LastIndexOf('(') < text.Length - 2)
- {
- int startPos = text.LastIndexOf('(') + 1;
- int length = text.Length - 1 - startPos;
-
- int.TryParse(text.Substring(startPos, length), out index);
- }
-
- return index;
- }
- }
-}
\ No newline at end of file
diff --git a/src/Umbraco.Core/PluginManager.cs b/src/Umbraco.Core/PluginManager.cs
index 3eb0f2a018..30029d2c87 100644
--- a/src/Umbraco.Core/PluginManager.cs
+++ b/src/Umbraco.Core/PluginManager.cs
@@ -44,8 +44,8 @@ namespace Umbraco.Core
private readonly object _typesLock = new object();
private readonly Dictionary _types = new Dictionary();
- private long _cachedAssembliesHash = -1;
- private long _currentAssembliesHash = -1;
+ private string _cachedAssembliesHash = null;
+ private string _currentAssembliesHash = null;
private IEnumerable _assemblies;
private bool _reportedChange;
@@ -75,9 +75,9 @@ namespace Umbraco.Core
if (detectChanges)
{
- //first check if the cached hash is 0, if it is then we ne
+ //first check if the cached hash is string.Empty, if it is then we need
//do the check if they've changed
- RequiresRescanning = (CachedAssembliesHash != CurrentAssembliesHash) || CachedAssembliesHash == 0;
+ RequiresRescanning = (CachedAssembliesHash != CurrentAssembliesHash) || CachedAssembliesHash == string.Empty;
//if they have changed, we need to write the new file
if (RequiresRescanning)
{
@@ -180,23 +180,20 @@ namespace Umbraco.Core
///
/// Gets the currently cached hash value of the scanned assemblies.
///
- /// The cached hash value, or 0 if no cache is found.
- internal long CachedAssembliesHash
+ /// The cached hash value, or string.Empty if no cache is found.
+ internal string CachedAssembliesHash
{
get
{
- if (_cachedAssembliesHash != -1)
+ if (_cachedAssembliesHash != null)
return _cachedAssembliesHash;
var filePath = GetPluginHashFilePath();
- if (File.Exists(filePath) == false) return 0;
+ if (File.Exists(filePath) == false) return string.Empty;
var hash = File.ReadAllText(filePath, Encoding.UTF8);
-
- long val;
- if (long.TryParse(hash, out val) == false) return 0;
-
- _cachedAssembliesHash = val;
+
+ _cachedAssembliesHash = hash;
return _cachedAssembliesHash;
}
}
@@ -205,11 +202,11 @@ namespace Umbraco.Core
/// Gets the current assemblies hash based on creating a hash from the assemblies in various places.
///
/// The current hash.
- internal long CurrentAssembliesHash
+ internal string CurrentAssembliesHash
{
get
{
- if (_currentAssembliesHash != -1)
+ if (_currentAssembliesHash != null)
return _currentAssembliesHash;
_currentAssembliesHash = GetFileHash(new List>
@@ -245,41 +242,40 @@ namespace Umbraco.Core
/// The hash.
/// Each file is a tuple containing the FileInfo object and a boolean which indicates whether to hash the
/// file properties (false) or the file contents (true).
- internal static long GetFileHash(IEnumerable> filesAndFolders, ProfilingLogger logger)
+ internal static string GetFileHash(IEnumerable> filesAndFolders, ProfilingLogger logger)
{
using (logger.TraceDuration("Determining hash of code files on disk", "Hash determined"))
- {
- var hashCombiner = new HashCodeCombiner();
-
+ {
// get the distinct file infos to hash
var uniqInfos = new HashSet();
var uniqContent = new HashSet();
-
- foreach (var fileOrFolder in filesAndFolders)
+ using (var generator = new HashGenerator())
{
- var info = fileOrFolder.Item1;
- if (fileOrFolder.Item2)
+ foreach (var fileOrFolder in filesAndFolders)
{
- // add each unique file's contents to the hash
- // normalize the content for cr/lf and case-sensitivity
-
- if (uniqContent.Contains(info.FullName)) continue;
- uniqContent.Add(info.FullName);
- if (File.Exists(info.FullName) == false) continue;
- var content = RemoveCrLf(File.ReadAllText(info.FullName));
- hashCombiner.AddCaseInsensitiveString(content);
+ var info = fileOrFolder.Item1;
+ if (fileOrFolder.Item2)
+ {
+ // add each unique file's contents to the hash
+ // normalize the content for cr/lf and case-sensitivity
+ if (uniqContent.Add(info.FullName))
+ {
+ if (File.Exists(info.FullName) == false) continue;
+ var content = RemoveCrLf(File.ReadAllText(info.FullName));
+ generator.AddCaseInsensitiveString(content);
+ }
+ }
+ else
+ {
+ // add each unique folder/file to the hash
+ if (uniqInfos.Add(info.FullName))
+ {
+ generator.AddFileSystemItem(info);
+ }
+ }
}
- else
- {
- // add each unique folder/file to the hash
-
- if (uniqInfos.Contains(info.FullName)) continue;
- uniqInfos.Add(info.FullName);
- hashCombiner.AddFileSystemItem(info);
- }
- }
-
- return ConvertHashToInt64(hashCombiner.GetCombinedHashCode());
+ return generator.GenerateHash();
+ }
}
}
@@ -303,39 +299,33 @@ namespace Umbraco.Core
/// A collection of files.
/// A profiling logger.
/// The hash.
- internal static long GetFileHash(IEnumerable filesAndFolders, ProfilingLogger logger)
+ internal static string GetFileHash(IEnumerable filesAndFolders, ProfilingLogger logger)
{
using (logger.TraceDuration("Determining hash of code files on disk", "Hash determined"))
{
- var hashCombiner = new HashCodeCombiner();
-
- // get the distinct file infos to hash
- var uniqInfos = new HashSet();
-
- foreach (var fileOrFolder in filesAndFolders)
+ using (var generator = new HashGenerator())
{
- if (uniqInfos.Contains(fileOrFolder.FullName)) continue;
- uniqInfos.Add(fileOrFolder.FullName);
- hashCombiner.AddFileSystemItem(fileOrFolder);
- }
+ // get the distinct file infos to hash
+ var uniqInfos = new HashSet();
- return ConvertHashToInt64(hashCombiner.GetCombinedHashCode());
+ foreach (var fileOrFolder in filesAndFolders)
+ {
+ if (uniqInfos.Contains(fileOrFolder.FullName)) continue;
+ uniqInfos.Add(fileOrFolder.FullName);
+ generator.AddFileSystemItem(fileOrFolder);
+ }
+ return generator.GenerateHash();
+ }
}
- }
-
- ///
- /// Converts a string hash value into an Int64.
- ///
- internal static long ConvertHashToInt64(string val)
- {
- long outVal;
- return long.TryParse(val, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out outVal) ? outVal : 0;
- }
+ }
#endregion
#region Cache
+ private const int ListFileOpenReadTimeout = 4000; // milliseconds
+ private const int ListFileOpenWriteTimeout = 2000; // milliseconds
+
///
/// Attemps to retrieve the list of types from the cache.
///
@@ -381,7 +371,7 @@ namespace Umbraco.Core
if (File.Exists(filePath) == false)
return cache;
- using (var stream = File.OpenRead(filePath))
+ using (var stream = GetFileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, ListFileOpenReadTimeout))
using (var reader = new StreamReader(stream))
{
while (true)
@@ -451,7 +441,7 @@ namespace Umbraco.Core
{
var filePath = GetPluginListFilePath();
- using (var stream = File.Open(filePath, FileMode.Create, FileAccess.ReadWrite))
+ using (var stream = GetFileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, ListFileOpenWriteTimeout))
using (var writer = new StreamWriter(stream))
{
foreach (var typeList in _types.Values)
@@ -471,7 +461,28 @@ namespace Umbraco.Core
// at the moment we write the cache to disk every time we update it. ideally we defer the writing
// since all the updates are going to happen in a row when Umbraco starts. that being said, the
// file is small enough, so it is not a priority.
- WriteCache();
+ WriteCache();
+ }
+
+ private static Stream GetFileStream(string path, FileMode fileMode, FileAccess fileAccess, FileShare fileShare, int timeoutMilliseconds)
+ {
+ const int pauseMilliseconds = 250;
+ var attempts = timeoutMilliseconds / pauseMilliseconds;
+ while (true)
+ {
+ try
+ {
+ return new FileStream(path, fileMode, fileAccess, fileShare);
+ }
+ catch
+ {
+ if (--attempts == 0)
+ throw;
+
+ LogHelper.Debug(string.Format("Attempted to get filestream for file {0} failed, {1} attempts left, pausing for {2} milliseconds", path, attempts, pauseMilliseconds));
+ Thread.Sleep(pauseMilliseconds);
+ }
+ }
}
#endregion
diff --git a/src/Umbraco.Core/PropertyEditors/PropertyEditorAttribute.cs b/src/Umbraco.Core/PropertyEditors/PropertyEditorAttribute.cs
index 41e4ccc74e..ec87af29ad 100644
--- a/src/Umbraco.Core/PropertyEditors/PropertyEditorAttribute.cs
+++ b/src/Umbraco.Core/PropertyEditors/PropertyEditorAttribute.cs
@@ -26,7 +26,7 @@ namespace Umbraco.Core.PropertyEditors
public PropertyEditorAttribute(string alias, string name)
{
- Mandate.ParameterNotNullOrEmpty(alias, "id");
+ Mandate.ParameterNotNullOrEmpty(alias, "alias");
Mandate.ParameterNotNullOrEmpty(name, "name");
Alias = alias;
@@ -82,4 +82,4 @@ namespace Umbraco.Core.PropertyEditors
///
public string Group { get; set; }
}
-}
\ No newline at end of file
+}
diff --git a/src/Umbraco.Core/PropertyEditors/PropertyValueEditor.cs b/src/Umbraco.Core/PropertyEditors/PropertyValueEditor.cs
index 0b083025f7..66a316fd0f 100644
--- a/src/Umbraco.Core/PropertyEditors/PropertyValueEditor.cs
+++ b/src/Umbraco.Core/PropertyEditors/PropertyValueEditor.cs
@@ -293,7 +293,7 @@ namespace Umbraco.Core.PropertyEditors
//swallow this exception, we thought it was json but it really isn't so continue returning a string
}
}
- return property.Value.ToString();
+ return asString;
case DataTypeDatabaseType.Integer:
case DataTypeDatabaseType.Decimal:
//Decimals need to be formatted with invariant culture (dots, not commas)
diff --git a/src/Umbraco.Core/Security/BackOfficeUserStore.cs b/src/Umbraco.Core/Security/BackOfficeUserStore.cs
index 7d5774c870..bb5fc0cabd 100644
--- a/src/Umbraco.Core/Security/BackOfficeUserStore.cs
+++ b/src/Umbraco.Core/Security/BackOfficeUserStore.cs
@@ -93,7 +93,7 @@ namespace Umbraco.Core.Security
{
//this will hash the guid with a salt so should be nicely random
var aspHasher = new PasswordHasher();
- member.RawPasswordValue = "___UIDEMPTYPWORD__" +
+ member.RawPasswordValue = Constants.Security.EmptyPasswordPrefix +
aspHasher.HashPassword(Guid.NewGuid().ToString("N"));
}
diff --git a/src/Umbraco.Core/Security/MembershipProviderBase.cs b/src/Umbraco.Core/Security/MembershipProviderBase.cs
index 4d4dad3fe8..2cef4a9aab 100644
--- a/src/Umbraco.Core/Security/MembershipProviderBase.cs
+++ b/src/Umbraco.Core/Security/MembershipProviderBase.cs
@@ -64,6 +64,19 @@ namespace Umbraco.Core.Security
{
get { return false; }
}
+
+ ///
+ /// Returns the raw password value for a given user
+ ///
+ ///
+ ///
+ ///
+ /// By default this will return an invalid attempt, inheritors will need to override this to support it
+ ///
+ protected virtual Attempt GetRawPassword(string username)
+ {
+ return Attempt.Fail();
+ }
private string _applicationName;
private bool _enablePasswordReset;
@@ -299,7 +312,7 @@ namespace Umbraco.Core.Security
/// Processes a request to update the password for a membership user.
///
/// The user to update the password for.
- /// This property is ignore for this provider
+ /// Required to change a user password if the user is not new and AllowManuallyChangingPassword is false
/// The new password for the specified user.
///
/// true if the password was updated successfully; otherwise, false.
@@ -309,10 +322,17 @@ namespace Umbraco.Core.Security
///
public override bool ChangePassword(string username, string oldPassword, string newPassword)
{
+ string rawPasswordValue = string.Empty;
if (oldPassword.IsNullOrWhiteSpace() && AllowManuallyChangingPassword == false)
- {
- //If the old password is empty and AllowManuallyChangingPassword is false, than this provider cannot just arbitrarily change the password
- throw new NotSupportedException("This provider does not support manually changing the password");
+ {
+ //we need to lookup the member since this could be a brand new member without a password set
+ var rawPassword = GetRawPassword(username);
+ rawPasswordValue = rawPassword.Success ? rawPassword.Result : string.Empty;
+ if (rawPassword.Success == false || rawPasswordValue.StartsWith(Constants.Security.EmptyPasswordPrefix) == false)
+ {
+ //If the old password is empty and AllowManuallyChangingPassword is false, than this provider cannot just arbitrarily change the password
+ throw new NotSupportedException("This provider does not support manually changing the password");
+ }
}
var args = new ValidatePasswordEventArgs(username, newPassword, false);
@@ -325,10 +345,14 @@ namespace Umbraco.Core.Security
throw new MembershipPasswordException("Change password canceled due to password validation failure.");
}
- //Special case to allow changing password without validating existing credentials
- //This is used during installation only
- if (AllowManuallyChangingPassword == false && ApplicationContext.Current != null
- && ApplicationContext.Current.IsConfigured == false && oldPassword == "default")
+ //Special cases to allow changing password without validating existing credentials
+ // * the member is new and doesn't have a password set
+ // * during installation to set the admin password
+ if (AllowManuallyChangingPassword == false
+ && (rawPasswordValue.StartsWith(Constants.Security.EmptyPasswordPrefix)
+ || (ApplicationContext.Current != null
+ && ApplicationContext.Current.IsConfigured == false
+ && oldPassword == "default")))
{
return PerformChangePassword(username, oldPassword, newPassword);
}
diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs
index 3cf8dfcff4..913a83c1cd 100644
--- a/src/Umbraco.Core/Services/ContentService.cs
+++ b/src/Umbraco.Core/Services/ContentService.cs
@@ -166,7 +166,8 @@ namespace Umbraco.Core.Services
using (var uow = UowProvider.GetUnitOfWork())
{
- if (uow.Events.DispatchCancelable(Creating, this, new NewEventArgs(content, contentTypeAlias, parentId)))
+ var newEventArgs = new NewEventArgs(content, contentTypeAlias, parentId);
+ if (uow.Events.DispatchCancelable(Creating, this, newEventArgs))
{
uow.Commit();
content.WasCancelled = true;
@@ -175,8 +176,8 @@ namespace Umbraco.Core.Services
content.CreatorId = userId;
content.WriterId = userId;
-
- uow.Events.Dispatch(Created, this, new NewEventArgs(content, false, contentTypeAlias, parentId));
+ newEventArgs.CanCancel = false;
+ uow.Events.Dispatch(Created, this, newEventArgs);
Audit(uow, AuditType.New, string.Format("Content '{0}' was created", name), content.CreatorId, content.Id);
uow.Commit();
@@ -209,7 +210,8 @@ namespace Umbraco.Core.Services
using (var uow = UowProvider.GetUnitOfWork())
{
- if (uow.Events.DispatchCancelable(Creating, this, new NewEventArgs(content, contentTypeAlias, parent)))
+ var newEventArgs = new NewEventArgs(content, contentTypeAlias, parent);
+ if (uow.Events.DispatchCancelable(Creating, this, newEventArgs))
{
uow.Commit();
content.WasCancelled = true;
@@ -218,8 +220,8 @@ namespace Umbraco.Core.Services
content.CreatorId = userId;
content.WriterId = userId;
-
- uow.Events.Dispatch(Created, this, new NewEventArgs(content, false, contentTypeAlias, parent));
+ newEventArgs.CanCancel = false;
+ uow.Events.Dispatch(Created, this, newEventArgs);
Audit(uow, AuditType.New, string.Format("Content '{0}' was created", name), content.CreatorId, content.Id);
uow.Commit();
@@ -250,14 +252,16 @@ namespace Umbraco.Core.Services
{
//NOTE: I really hate the notion of these Creating/Created events - they are so inconsistent, I've only just found
// out that in these 'WithIdentity' methods, the Saving/Saved events were not fired, wtf. Anyways, they're added now.
- if (uow.Events.DispatchCancelable(Creating, this, new NewEventArgs(content, contentTypeAlias, parentId)))
+ var newEventArgs = new NewEventArgs(content, contentTypeAlias, parentId);
+ if (uow.Events.DispatchCancelable(Creating, this, newEventArgs))
{
uow.Commit();
content.WasCancelled = true;
return content;
}
- if (uow.Events.DispatchCancelable(Saving, this, new SaveEventArgs(content)))
+ var saveEventArgs = new SaveEventArgs(content);
+ if (uow.Events.DispatchCancelable(Saving, this, saveEventArgs))
{
uow.Commit();
content.WasCancelled = true;
@@ -270,9 +274,10 @@ namespace Umbraco.Core.Services
repository.AddOrUpdate(content);
repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c));
-
- uow.Events.Dispatch(Saved, this, new SaveEventArgs(content, false));
- uow.Events.Dispatch(Created, this, new NewEventArgs(content, false, contentTypeAlias, parentId));
+ saveEventArgs.CanCancel = false;
+ uow.Events.Dispatch(Saved, this, saveEventArgs);
+ newEventArgs.CanCancel = false;
+ uow.Events.Dispatch(Created, this, newEventArgs);
Audit(uow, AuditType.New, string.Format("Content '{0}' was created with Id {1}", name, content.Id), content.CreatorId, content.Id);
uow.Commit();
@@ -305,14 +310,16 @@ namespace Umbraco.Core.Services
{
//NOTE: I really hate the notion of these Creating/Created events - they are so inconsistent, I've only just found
// out that in these 'WithIdentity' methods, the Saving/Saved events were not fired, wtf. Anyways, they're added now.
- if (uow.Events.DispatchCancelable(Creating, this, new NewEventArgs(content, contentTypeAlias, parent)))
+ var newEventArgs = new NewEventArgs(content, contentTypeAlias, parent);
+ if (uow.Events.DispatchCancelable(Creating, this, newEventArgs))
{
uow.Commit();
content.WasCancelled = true;
return content;
}
- if (uow.Events.DispatchCancelable(Saving, this, new SaveEventArgs(content)))
+ var saveEventArgs = new SaveEventArgs(content);
+ if (uow.Events.DispatchCancelable(Saving, this, saveEventArgs))
{
uow.Commit();
content.WasCancelled = true;
@@ -325,9 +332,10 @@ namespace Umbraco.Core.Services
repository.AddOrUpdate(content);
repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c));
-
- uow.Events.Dispatch(Saved, this, new SaveEventArgs(content, false));
- uow.Events.Dispatch(Created, this, new NewEventArgs(content, false, contentTypeAlias, parent));
+ saveEventArgs.CanCancel = false;
+ uow.Events.Dispatch(Saved, this, saveEventArgs);
+ newEventArgs.CanCancel = false;
+ uow.Events.Dispatch(Created, this, newEventArgs);
Audit(uow, AuditType.New, string.Format("Content '{0}' was created with Id {1}", name, content.Id), content.CreatorId, content.Id);
uow.Commit();
@@ -895,15 +903,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;
+ }
}
///
@@ -1018,14 +1041,16 @@ namespace Umbraco.Core.Services
//see: http://issues.umbraco.org/issue/U4-9336
content.EnsureValidPath(Logger, entity => GetById(entity.ParentId), QuickUpdate);
var originalPath = content.Path;
- if (uow.Events.DispatchCancelable(Trashing, this, new MoveEventArgs(evtMsgs, new MoveEventInfo(content, originalPath, Constants.System.RecycleBinContent)), "Trashing"))
+ var moveEventInfo = new MoveEventInfo(content, originalPath, Constants.System.RecycleBinContent);
+ var moveEventArgs = new MoveEventArgs(evtMsgs, moveEventInfo);
+ if (uow.Events.DispatchCancelable(Trashing, this, moveEventArgs, "Trashing"))
{
uow.Commit();
return OperationStatus.Cancelled(evtMsgs);
}
var moveInfo = new List>
{
- new MoveEventInfo(content, originalPath, Constants.System.RecycleBinContent)
+ moveEventInfo
};
//get descendents to process of the content item that is being moved to trash - must be done before changing the state below
@@ -1056,7 +1081,9 @@ namespace Umbraco.Core.Services
moveInfo.Add(new MoveEventInfo(descendant, descendant.Path, descendant.ParentId));
}
- uow.Events.Dispatch(Trashed, this, new MoveEventArgs(false, evtMsgs, moveInfo.ToArray()), "Trashed");
+ moveEventArgs.CanCancel = false;
+ moveEventArgs.MoveInfoCollection = moveInfo;
+ uow.Events.Dispatch(Trashed, this, moveEventArgs, "Trashed");
Audit(uow, AuditType.Move, "Move Content to Recycle Bin performed by user", userId, content.Id);
uow.Commit();
@@ -1183,7 +1210,8 @@ namespace Umbraco.Core.Services
using (var uow = UowProvider.GetUnitOfWork())
{
- if (raiseEvents && uow.Events.DispatchCancelable(Saving, this, new SaveEventArgs(asArray, evtMsgs)))
+ var saveEventArgs = new SaveEventArgs(asArray, evtMsgs);
+ if (raiseEvents && uow.Events.DispatchCancelable(Saving, this, saveEventArgs))
{
uow.Commit();
return OperationStatus.Cancelled(evtMsgs);
@@ -1224,7 +1252,10 @@ namespace Umbraco.Core.Services
}
if (raiseEvents)
- uow.Events.Dispatch(Saved, this, new SaveEventArgs(asArray, false, evtMsgs));
+ {
+ saveEventArgs.CanCancel = false;
+ uow.Events.Dispatch(Saved, this, saveEventArgs);
+ }
Audit(uow, AuditType.Save, "Bulk Save content performed by user", userId == -1 ? 0 : userId, Constants.System.Root);
uow.Commit();
@@ -1250,7 +1281,8 @@ namespace Umbraco.Core.Services
{
using (var uow = UowProvider.GetUnitOfWork())
{
- if (uow.Events.DispatchCancelable(Deleting, this, new DeleteEventArgs(content, evtMsgs), "Deleting"))
+ var deleteEventArgs = new DeleteEventArgs(content, evtMsgs);
+ if (uow.Events.DispatchCancelable(Deleting, this, deleteEventArgs, "Deleting"))
{
uow.Commit();
return OperationStatus.Cancelled(evtMsgs);
@@ -1273,8 +1305,8 @@ namespace Umbraco.Core.Services
repository.Delete(content);
- var args = new DeleteEventArgs(content, false, evtMsgs);
- uow.Events.Dispatch(Deleted, this, args, "Deleted");
+ deleteEventArgs.CanCancel = false;
+ uow.Events.Dispatch(Deleted, this, deleteEventArgs, "Deleted");
Audit(uow, AuditType.Delete, "Delete Content performed by user", userId, content.Id);
uow.Commit();
@@ -1402,7 +1434,8 @@ namespace Umbraco.Core.Services
{
using (var uow = UowProvider.GetUnitOfWork())
{
- if (uow.Events.DispatchCancelable(DeletingVersions, this, new DeleteRevisionsEventArgs(id, dateToRetain: versionDate), "DeletingVersions"))
+ var deleteRevisionsEventArgs = new DeleteRevisionsEventArgs(id, dateToRetain: versionDate);
+ if (uow.Events.DispatchCancelable(DeletingVersions, this, deleteRevisionsEventArgs, "DeletingVersions"))
{
uow.Commit();
return;
@@ -1410,8 +1443,8 @@ namespace Umbraco.Core.Services
var repository = RepositoryFactory.CreateContentRepository(uow);
repository.DeleteVersions(id, versionDate);
-
- uow.Events.Dispatch(DeletedVersions, this, new DeleteRevisionsEventArgs(id, false, dateToRetain: versionDate), "DeletedVersions");
+ deleteRevisionsEventArgs.CanCancel = false;
+ uow.Events.Dispatch(DeletedVersions, this, deleteRevisionsEventArgs, "DeletedVersions");
Audit(uow, AuditType.Delete, "Delete Content by version date performed by user", userId, Constants.System.Root);
uow.Commit();
@@ -1490,7 +1523,9 @@ namespace Umbraco.Core.Services
using (var uow = UowProvider.GetUnitOfWork())
{
- if (uow.Events.DispatchCancelable(Moving, this, new MoveEventArgs(new MoveEventInfo(content, content.Path, parentId)), "Moving"))
+ var moveEventInfo = new MoveEventInfo(content, content.Path, parentId);
+ var moveEventArgs = new MoveEventArgs(moveEventInfo);
+ if (uow.Events.DispatchCancelable(Moving, this, moveEventArgs, "Moving"))
{
uow.Commit();
return;
@@ -1502,7 +1537,9 @@ namespace Umbraco.Core.Services
//call private method that does the recursive moving
PerformMove(content, parentId, userId, moveInfo);
- uow.Events.Dispatch(Moved, this, new MoveEventArgs(false, moveInfo.ToArray()), "Moved");
+ moveEventArgs.MoveInfoCollection = moveInfo;
+ moveEventArgs.CanCancel = false;
+ uow.Events.Dispatch(Moved, this, moveEventArgs, "Moved");
Audit(uow, AuditType.Move, "Move Content performed by user", userId, content.Id);
uow.Commit();
@@ -1531,15 +1568,17 @@ namespace Umbraco.Core.Services
var files = ((ContentRepository)repository).GetFilesInRecycleBinForUploadField();
- if (uow.Events.DispatchCancelable(EmptyingRecycleBin, this, new RecycleBinEventArgs(nodeObjectType, entities, files)))
+ var recycleBinEventArgs = new RecycleBinEventArgs(nodeObjectType, entities, files);
+ if (uow.Events.DispatchCancelable(EmptyingRecycleBin, this, recycleBinEventArgs))
{
uow.Commit();
return;
}
var success = repository.EmptyRecycleBin();
-
- uow.Events.Dispatch(EmptiedRecycleBin, this, new RecycleBinEventArgs(nodeObjectType, entities, files, success));
+ recycleBinEventArgs.CanCancel = false;
+ recycleBinEventArgs.RecycleBinEmptiedSuccessfully = success;
+ uow.Events.Dispatch(EmptiedRecycleBin, this, recycleBinEventArgs);
Audit(uow, AuditType.Delete, "Empty Content Recycle Bin performed by user", 0, Constants.System.RecycleBinContent);
uow.Commit();
@@ -1586,7 +1625,8 @@ namespace Umbraco.Core.Services
using (var uow = UowProvider.GetUnitOfWork())
{
- if (uow.Events.DispatchCancelable(Copying, this, new CopyEventArgs(content, copy, parentId)))
+ var copyEventArgs = new CopyEventArgs(content, copy, true, parentId, relateToOriginal);
+ if (uow.Events.DispatchCancelable(Copying, this, copyEventArgs))
{
uow.Commit();
return null;
@@ -1627,7 +1667,8 @@ namespace Umbraco.Core.Services
Copy(child, copy.Id, relateToOriginal, true, userId);
}
}
- uow.Events.Dispatch(Copied, this, new CopyEventArgs(content, copy, false, parentId, relateToOriginal));
+ copyEventArgs.CanCancel = false;
+ uow.Events.Dispatch(Copied, this, copyEventArgs);
Audit(uow, AuditType.Copy, "Copy Content performed by user", content.WriterId, content.Id);
uow.Commit();
}
@@ -1647,7 +1688,8 @@ namespace Umbraco.Core.Services
{
using (var uow = UowProvider.GetUnitOfWork())
{
- if (uow.Events.DispatchCancelable(SendingToPublish, this, new SendToPublishEventArgs(content)))
+ var sendToPublishEventArgs = new SendToPublishEventArgs(content);
+ if (uow.Events.DispatchCancelable(SendingToPublish, this, sendToPublishEventArgs))
{
uow.Commit();
return false;
@@ -1655,8 +1697,8 @@ namespace Umbraco.Core.Services
//Save before raising event
Save(content, userId);
-
- uow.Events.Dispatch(SentToPublish, this, new SendToPublishEventArgs(content, false));
+ sendToPublishEventArgs.CanCancel = false;
+ uow.Events.Dispatch(SentToPublish, this, sendToPublishEventArgs);
Audit(uow, AuditType.SendToPublish, "Send to Publish performed by user", content.WriterId, content.Id);
uow.Commit();
@@ -1683,7 +1725,8 @@ namespace Umbraco.Core.Services
using (var uow = UowProvider.GetUnitOfWork())
{
- if (uow.Events.DispatchCancelable(RollingBack, this, new RollbackEventArgs(content)))
+ var rollbackEventArgs = new RollbackEventArgs(content);
+ if (uow.Events.DispatchCancelable(RollingBack, this, rollbackEventArgs))
{
uow.Commit();
return content;
@@ -1697,8 +1740,8 @@ namespace Umbraco.Core.Services
repository.AddOrUpdate(content);
repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c));
-
- uow.Events.Dispatch(RolledBack, this, new RollbackEventArgs(content, false));
+ rollbackEventArgs.CanCancel = false;
+ uow.Events.Dispatch(RolledBack, this, rollbackEventArgs);
Audit(uow, AuditType.RollBack, "Content rollback performed by user", content.WriterId, content.Id);
uow.Commit();
@@ -1729,7 +1772,8 @@ namespace Umbraco.Core.Services
using (var uow = UowProvider.GetUnitOfWork())
{
var asArray = items.ToArray();
- if (raiseEvents && uow.Events.DispatchCancelable(Saving, this, new SaveEventArgs(asArray)))
+ var saveEventArgs = new SaveEventArgs(asArray);
+ if (raiseEvents && uow.Events.DispatchCancelable(Saving, this, saveEventArgs))
{
uow.Commit();
return false;
@@ -1774,7 +1818,10 @@ namespace Umbraco.Core.Services
}
if (raiseEvents)
- uow.Events.Dispatch(Saved, this, new SaveEventArgs(asArray, false));
+ {
+ saveEventArgs.CanCancel = false;
+ uow.Events.Dispatch(Saved, this, saveEventArgs);
+ }
if (shouldBePublished.Any())
{
@@ -2138,7 +2185,8 @@ namespace Umbraco.Core.Services
{
using (var uow = UowProvider.GetUnitOfWork())
{
- if (raiseEvents && uow.Events.DispatchCancelable(Saving, this, new SaveEventArgs(content, evtMsgs)))
+ var saveEventArgs = new SaveEventArgs(content, evtMsgs);
+ if (raiseEvents && uow.Events.DispatchCancelable(Saving, this, saveEventArgs))
{
uow.Commit();
return Attempt.Fail(new PublishStatus(content, PublishStatusType.FailedCancelledByEvent, evtMsgs));
@@ -2193,7 +2241,10 @@ namespace Umbraco.Core.Services
}
if (raiseEvents)
- uow.Events.Dispatch(Saved, this, new SaveEventArgs(content, false, evtMsgs));
+ {
+ saveEventArgs.CanCancel = false;
+ uow.Events.Dispatch(Saved, this, saveEventArgs);
+ }
//Save xml to db and call following method to fire event through PublishingStrategy to update cache
if (published)
@@ -2232,7 +2283,8 @@ namespace Umbraco.Core.Services
{
using (var uow = UowProvider.GetUnitOfWork())
{
- if (raiseEvents && uow.Events.DispatchCancelable(Saving, this, new SaveEventArgs(content, evtMsgs)))
+ var saveEventArgs = new SaveEventArgs(content, evtMsgs);
+ if (raiseEvents && uow.Events.DispatchCancelable(Saving, this, saveEventArgs))
{
uow.Commit();
return OperationStatus.Cancelled(evtMsgs);
@@ -2261,7 +2313,10 @@ namespace Umbraco.Core.Services
repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c));
if (raiseEvents)
- uow.Events.Dispatch(Saved, this, new SaveEventArgs(content, false, evtMsgs));
+ {
+ saveEventArgs.CanCancel = false;
+ uow.Events.Dispatch(Saved, this, saveEventArgs);
+ }
Audit(uow, AuditType.Save, "Save Content performed by user", userId, content.Id);
uow.Commit();
@@ -2271,40 +2326,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/ContentTypeService.cs b/src/Umbraco.Core/Services/ContentTypeService.cs
index 1b6735153d..14ba9f30bf 100644
--- a/src/Umbraco.Core/Services/ContentTypeService.cs
+++ b/src/Umbraco.Core/Services/ContentTypeService.cs
@@ -54,7 +54,8 @@ namespace Umbraco.Core.Services
CreatorId = userId
};
- if (uow.Events.DispatchCancelable(SavingContentTypeContainer, this, new SaveEventArgs(container, evtMsgs)))
+ var saveEventArgs = new SaveEventArgs(container, evtMsgs);
+ if (uow.Events.DispatchCancelable(SavingContentTypeContainer, this, saveEventArgs))
{
uow.Commit();
return Attempt.Fail(new OperationStatus(container, OperationStatusType.FailedCancelledByEvent, evtMsgs));
@@ -62,8 +63,8 @@ namespace Umbraco.Core.Services
repo.AddOrUpdate(container);
uow.Commit();
-
- uow.Events.Dispatch(SavedContentTypeContainer, this, new SaveEventArgs(container, evtMsgs), "SavedContentTypeContainer");
+ saveEventArgs.CanCancel = false;
+ uow.Events.Dispatch(SavedContentTypeContainer, this, saveEventArgs, "SavedContentTypeContainer");
//TODO: Audit trail ?
return Attempt.Succeed(new OperationStatus(container, OperationStatusType.Success, evtMsgs));
@@ -92,7 +93,8 @@ namespace Umbraco.Core.Services
CreatorId = userId
};
- if (uow.Events.DispatchCancelable(SavingMediaTypeContainer, this, new SaveEventArgs(container, evtMsgs)))
+ var saveEventArgs = new SaveEventArgs(container, evtMsgs);
+ if (uow.Events.DispatchCancelable(SavingMediaTypeContainer, this, saveEventArgs))
{
uow.Commit();
return Attempt.Fail(new OperationStatus(container, OperationStatusType.FailedCancelledByEvent, evtMsgs));
@@ -100,8 +102,8 @@ namespace Umbraco.Core.Services
repo.AddOrUpdate(container);
uow.Commit();
-
- uow.Events.Dispatch(SavedMediaTypeContainer, this, new SaveEventArgs(container, evtMsgs), "SavedMediaTypeContainer");
+ saveEventArgs.CanCancel = false;
+ uow.Events.Dispatch(SavedMediaTypeContainer, this, saveEventArgs, "SavedMediaTypeContainer");
//TODO: Audit trail ?
return Attempt.Succeed(new OperationStatus(container, OperationStatusType.Success, evtMsgs));
@@ -289,7 +291,8 @@ namespace Umbraco.Core.Services
return OperationStatus.NoOperation(evtMsgs);
}
- if (uow.Events.DispatchCancelable(DeletingContentTypeContainer, this, new DeleteEventArgs(container, evtMsgs)))
+ var deleteEventArgs = new DeleteEventArgs(container, evtMsgs);
+ if (uow.Events.DispatchCancelable(DeletingContentTypeContainer, this, deleteEventArgs))
{
uow.Commit();
return Attempt.Fail(new OperationStatus(OperationStatusType.FailedCancelledByEvent, evtMsgs));
@@ -297,8 +300,8 @@ namespace Umbraco.Core.Services
repo.Delete(container);
uow.Commit();
-
- uow.Events.Dispatch(DeletedContentTypeContainer, this, new DeleteEventArgs(container, evtMsgs), "DeletedContentTypeContainer");
+ deleteEventArgs.CanCancel = false;
+ uow.Events.Dispatch(DeletedContentTypeContainer, this, deleteEventArgs, "DeletedContentTypeContainer");
return OperationStatus.Success(evtMsgs);
//TODO: Audit trail ?
@@ -319,7 +322,8 @@ namespace Umbraco.Core.Services
return OperationStatus.NoOperation(evtMsgs);
}
- if (uow.Events.DispatchCancelable(DeletingMediaTypeContainer, this, new DeleteEventArgs(container, evtMsgs)))
+ var deleteEventArgs = new DeleteEventArgs(container, evtMsgs);
+ if (uow.Events.DispatchCancelable(DeletingMediaTypeContainer, this, deleteEventArgs))
{
uow.Commit();
return Attempt.Fail(new OperationStatus(OperationStatusType.FailedCancelledByEvent, evtMsgs));
@@ -327,8 +331,8 @@ namespace Umbraco.Core.Services
repo.Delete(container);
uow.Commit();
-
- uow.Events.Dispatch(DeletedMediaTypeContainer, this, new DeleteEventArgs(container, evtMsgs));
+ deleteEventArgs.CanCancel = false;
+ uow.Events.Dispatch(DeletedMediaTypeContainer, this, deleteEventArgs);
return OperationStatus.Success(evtMsgs);
//TODO: Audit trail ?
@@ -756,7 +760,8 @@ namespace Umbraco.Core.Services
{
using (var uow = UowProvider.GetUnitOfWork())
{
- if (uow.Events.DispatchCancelable(SavingContentType, this, new SaveEventArgs(contentType)))
+ var saveEventArgs = new SaveEventArgs(contentType);
+ if (uow.Events.DispatchCancelable(SavingContentType, this, saveEventArgs))
{
uow.Commit();
return;
@@ -779,7 +784,8 @@ namespace Umbraco.Core.Services
UpdateContentXmlStructure(contentType);
- uow.Events.Dispatch(SavedContentType, this, new SaveEventArgs(contentType, false));
+ saveEventArgs.CanCancel = false;
+ uow.Events.Dispatch(SavedContentType, this, saveEventArgs);
Audit(uow, AuditType.Save, "Save ContentType performed by user", userId, contentType.Id);
uow.Commit();
@@ -800,7 +806,8 @@ namespace Umbraco.Core.Services
{
using (var uow = UowProvider.GetUnitOfWork())
{
- if (uow.Events.DispatchCancelable(SavingContentType, this, new SaveEventArgs(asArray)))
+ var saveEventArgs = new SaveEventArgs(asArray);
+ if (uow.Events.DispatchCancelable(SavingContentType, this, saveEventArgs))
{
uow.Commit();
return;
@@ -825,7 +832,8 @@ namespace Umbraco.Core.Services
UpdateContentXmlStructure(asArray.Cast().ToArray());
- uow.Events.Dispatch(SavedContentType, this, new SaveEventArgs(asArray, false));
+ saveEventArgs.CanCancel = false;
+ uow.Events.Dispatch(SavedContentType, this, saveEventArgs);
Audit(uow, AuditType.Save, "Save ContentTypes performed by user", userId, -1);
uow.Commit();
@@ -845,7 +853,8 @@ namespace Umbraco.Core.Services
{
using (var uow = UowProvider.GetUnitOfWork())
{
- if (uow.Events.DispatchCancelable(DeletingContentType, this, new DeleteEventArgs(contentType)))
+ var deleteEventArgs = new DeleteEventArgs(contentType);
+ if (uow.Events.DispatchCancelable(DeletingContentType, this, deleteEventArgs))
{
uow.Commit();
return;
@@ -860,8 +869,9 @@ namespace Umbraco.Core.Services
_contentService.DeleteContentOfTypes(deletedContentTypes.Select(x => x.Id), userId);
repository.Delete(contentType);
-
- uow.Events.Dispatch(DeletedContentType, this, new DeleteEventArgs(deletedContentTypes.DistinctBy(x => x.Id), false));
+ deleteEventArgs.DeletedEntities = deletedContentTypes.DistinctBy(x => x.Id);
+ deleteEventArgs.CanCancel = false;
+ uow.Events.Dispatch(DeletedContentType, this, deleteEventArgs);
Audit(uow, AuditType.Delete, string.Format("Delete ContentType performed by user"), userId, contentType.Id);
uow.Commit();
@@ -885,7 +895,8 @@ namespace Umbraco.Core.Services
{
using (var uow = UowProvider.GetUnitOfWork())
{
- if (uow.Events.DispatchCancelable(DeletingContentType, this, new DeleteEventArgs(asArray)))
+ var deleteEventArgs = new DeleteEventArgs(asArray);
+ if (uow.Events.DispatchCancelable(DeletingContentType, this, deleteEventArgs))
{
uow.Commit();
return;
@@ -906,8 +917,9 @@ namespace Umbraco.Core.Services
{
repository.Delete(contentType);
}
-
- uow.Events.Dispatch(DeletedContentType, this, new DeleteEventArgs(deletedContentTypes.DistinctBy(x => x.Id), false));
+ deleteEventArgs.DeletedEntities = deletedContentTypes.DistinctBy(x => x.Id);
+ deleteEventArgs.CanCancel = false;
+ uow.Events.Dispatch(DeletedContentType, this, deleteEventArgs);
Audit(uow, AuditType.Delete, string.Format("Delete ContentTypes performed by user"), userId, -1);
uow.Commit();
@@ -1056,7 +1068,9 @@ namespace Umbraco.Core.Services
var moveInfo = new List>();
using (var uow = UowProvider.GetUnitOfWork())
{
- if (uow.Events.DispatchCancelable(MovingMediaType, this, new MoveEventArgs(evtMsgs, new MoveEventInfo(toMove, toMove.Path, containerId))))
+ var moveEventInfo = new MoveEventInfo(toMove, toMove.Path, containerId);
+ var moveEventArgs = new MoveEventArgs(evtMsgs, moveEventInfo);
+ if (uow.Events.DispatchCancelable(MovingMediaType, this, moveEventArgs))
{
uow.Commit();
return Attempt.Fail(new OperationStatus(MoveOperationStatusType.FailedCancelledByEvent, evtMsgs));
@@ -1081,7 +1095,9 @@ namespace Umbraco.Core.Services
return Attempt.Fail(new OperationStatus(ex.Operation, evtMsgs));
}
uow.Commit();
- uow.Events.Dispatch(MovedMediaType, this, new MoveEventArgs(false, evtMsgs, moveInfo.ToArray()));
+ moveEventArgs.MoveInfoCollection = moveInfo;
+ moveEventArgs.CanCancel = false;
+ uow.Events.Dispatch(MovedMediaType, this, moveEventArgs);
}
return Attempt.Succeed(
@@ -1095,7 +1111,9 @@ namespace Umbraco.Core.Services
var moveInfo = new List>();
using (var uow = UowProvider.GetUnitOfWork())
{
- if (uow.Events.DispatchCancelable(MovingContentType, this, new MoveEventArgs(evtMsgs, new MoveEventInfo(toMove, toMove.Path, containerId))))
+ var moveEventInfo = new MoveEventInfo(toMove, toMove.Path, containerId);
+ var moveEventArgs = new MoveEventArgs(evtMsgs, moveEventInfo);
+ if (uow.Events.DispatchCancelable(MovingContentType, this, moveEventArgs))
{
uow.Commit();
return Attempt.Fail(new OperationStatus(MoveOperationStatusType.FailedCancelledByEvent, evtMsgs));
@@ -1119,8 +1137,10 @@ namespace Umbraco.Core.Services
uow.Commit();
return Attempt.Fail(new OperationStatus(ex.Operation, evtMsgs));
}
+ moveEventArgs.MoveInfoCollection = moveInfo;
+ moveEventArgs.CanCancel = false;
uow.Commit();
- uow.Events.Dispatch(MovedContentType, this, new MoveEventArgs(false, evtMsgs, moveInfo.ToArray()));
+ uow.Events.Dispatch(MovedContentType, this, moveEventArgs);
}
return Attempt.Succeed(
@@ -1228,7 +1248,8 @@ namespace Umbraco.Core.Services
{
using (var uow = UowProvider.GetUnitOfWork())
{
- if (uow.Events.DispatchCancelable(SavingMediaType, this, new SaveEventArgs(mediaType)))
+ var saveEventArgs = new SaveEventArgs(mediaType);
+ if (uow.Events.DispatchCancelable(SavingMediaType, this, saveEventArgs))
{
uow.Commit();
return;
@@ -1244,8 +1265,8 @@ namespace Umbraco.Core.Services
uow.Commit();
UpdateContentXmlStructure(mediaType);
-
- uow.Events.Dispatch(SavedMediaType, this, new SaveEventArgs(mediaType, false));
+ saveEventArgs.CanCancel = false;
+ uow.Events.Dispatch(SavedMediaType, this, saveEventArgs);
Audit(uow, AuditType.Save, "Save MediaType performed by user", userId, mediaType.Id);
uow.Commit();
@@ -1266,7 +1287,8 @@ namespace Umbraco.Core.Services
{
using (var uow = UowProvider.GetUnitOfWork())
{
- if (uow.Events.DispatchCancelable(SavingMediaType, this, new SaveEventArgs(asArray)))
+ var saveEventArgs = new SaveEventArgs(asArray);
+ if (uow.Events.DispatchCancelable(SavingMediaType, this, saveEventArgs))
{
uow.Commit();
return;
@@ -1290,8 +1312,8 @@ namespace Umbraco.Core.Services
uow.Commit();
UpdateContentXmlStructure(asArray.Cast().ToArray());
-
- uow.Events.Dispatch(SavedMediaType, this, new SaveEventArgs(asArray, false));
+ saveEventArgs.CanCancel = false;
+ uow.Events.Dispatch(SavedMediaType, this, saveEventArgs);
Audit(uow, AuditType.Save, "Save MediaTypes performed by user", userId, -1);
uow.Commit();
@@ -1313,7 +1335,8 @@ namespace Umbraco.Core.Services
{
using (var uow = UowProvider.GetUnitOfWork())
{
- if (uow.Events.DispatchCancelable(DeletingMediaType, this, new DeleteEventArgs(mediaType)))
+ var deleteEventArgs = new DeleteEventArgs(mediaType);
+ if (uow.Events.DispatchCancelable(DeletingMediaType, this, deleteEventArgs))
{
uow.Commit();
return;
@@ -1329,7 +1352,9 @@ namespace Umbraco.Core.Services
repository.Delete(mediaType);
- uow.Events.Dispatch(DeletedMediaType, this, new DeleteEventArgs(deletedMediaTypes.DistinctBy(x => x.Id), false));
+ deleteEventArgs.CanCancel = false;
+ deleteEventArgs.DeletedEntities = deletedMediaTypes.DistinctBy(x => x.Id);
+ uow.Events.Dispatch(DeletedMediaType, this, deleteEventArgs);
Audit(uow, AuditType.Delete, "Delete MediaType performed by user", userId, mediaType.Id);
uow.Commit();
@@ -1353,7 +1378,8 @@ namespace Umbraco.Core.Services
{
using (var uow = UowProvider.GetUnitOfWork())
{
- if (uow.Events.DispatchCancelable(DeletingMediaType, this, new DeleteEventArgs(asArray)))
+ var deleteEventArgs = new DeleteEventArgs(asArray);
+ if (uow.Events.DispatchCancelable(DeletingMediaType, this, deleteEventArgs))
{
uow.Commit();
return;
@@ -1375,7 +1401,9 @@ namespace Umbraco.Core.Services
repository.Delete(mediaType);
}
- uow.Events.Dispatch(DeletedMediaType, this, new DeleteEventArgs(deletedMediaTypes.DistinctBy(x => x.Id), false));
+ deleteEventArgs.DeletedEntities = deletedMediaTypes.DistinctBy(x => x.Id);
+ deleteEventArgs.CanCancel = false;
+ uow.Events.Dispatch(DeletedMediaType, this, deleteEventArgs);
Audit(uow, AuditType.Delete, "Delete MediaTypes performed by user", userId, -1);
uow.Commit();
diff --git a/src/Umbraco.Core/Services/DataTypeService.cs b/src/Umbraco.Core/Services/DataTypeService.cs
index c01b13136e..0ee3e9b823 100644
--- a/src/Umbraco.Core/Services/DataTypeService.cs
+++ b/src/Umbraco.Core/Services/DataTypeService.cs
@@ -312,7 +312,9 @@ namespace Umbraco.Core.Services
var moveInfo = new List>();
using (var uow = UowProvider.GetUnitOfWork())
{
- if (uow.Events.DispatchCancelable(Moving, this, new MoveEventArgs(evtMsgs, new MoveEventInfo(toMove, toMove.Path, parentId))))
+ var moveEventInfo = new MoveEventInfo(toMove, toMove.Path, parentId);
+ var moveEventArgs = new MoveEventArgs(evtMsgs, moveEventInfo);
+ if (uow.Events.DispatchCancelable(Moving, this, moveEventArgs))
{
uow.Commit();
return Attempt.Fail(new OperationStatus(MoveOperationStatusType.FailedCancelledByEvent, evtMsgs));
@@ -338,7 +340,9 @@ namespace Umbraco.Core.Services
new OperationStatus(ex.Operation, evtMsgs));
}
uow.Commit();
- uow.Events.Dispatch(Moved, this, new MoveEventArgs(false, evtMsgs, moveInfo.ToArray()));
+ moveEventArgs.MoveInfoCollection = moveInfo;
+ moveEventArgs.CanCancel = false;
+ uow.Events.Dispatch(Moved, this, moveEventArgs);
}
return Attempt.Succeed(
@@ -354,7 +358,8 @@ namespace Umbraco.Core.Services
{
using (var uow = UowProvider.GetUnitOfWork())
{
- if (uow.Events.DispatchCancelable(Saving, this, new SaveEventArgs(dataTypeDefinition)))
+ var saveEventArgs = new SaveEventArgs(dataTypeDefinition);
+ if (uow.Events.DispatchCancelable(Saving, this, saveEventArgs))
{
uow.Commit();
return;
@@ -369,8 +374,8 @@ namespace Umbraco.Core.Services
dataTypeDefinition.CreatorId = userId;
repository.AddOrUpdate(dataTypeDefinition);
-
- uow.Events.Dispatch(Saved, this, new SaveEventArgs(dataTypeDefinition, false));
+ saveEventArgs.CanCancel = false;
+ uow.Events.Dispatch(Saved, this, saveEventArgs);
Audit(uow, AuditType.Save, "Save DataTypeDefinition performed by user", userId, dataTypeDefinition.Id);
uow.Commit();
@@ -397,9 +402,10 @@ namespace Umbraco.Core.Services
{
using (var uow = UowProvider.GetUnitOfWork())
{
+ var saveEventArgs = new SaveEventArgs(dataTypeDefinitions);
if (raiseEvents)
- {
- if (uow.Events.DispatchCancelable(Saving, this, new SaveEventArgs(dataTypeDefinitions)))
+ {
+ if (uow.Events.DispatchCancelable(Saving, this, saveEventArgs))
{
uow.Commit();
return;
@@ -415,7 +421,10 @@ namespace Umbraco.Core.Services
}
if (raiseEvents)
- uow.Events.Dispatch(Saved, this, new SaveEventArgs(dataTypeDefinitions, false));
+ {
+ saveEventArgs.CanCancel = false;
+ uow.Events.Dispatch(Saved, this, saveEventArgs);
+ }
Audit(uow, AuditType.Save, string.Format("Save DataTypeDefinition performed by user"), userId, -1);
uow.Commit();
@@ -504,7 +513,8 @@ namespace Umbraco.Core.Services
{
using (var uow = UowProvider.GetUnitOfWork())
{
- if (uow.Events.DispatchCancelable(Saving, this, new SaveEventArgs(dataTypeDefinition)))
+ var saveEventArgs = new SaveEventArgs(dataTypeDefinition);
+ if (uow.Events.DispatchCancelable(Saving, this, saveEventArgs))
{
uow.Commit();
return;
@@ -526,8 +536,8 @@ namespace Umbraco.Core.Services
Audit(uow, AuditType.Save, string.Format("Save DataTypeDefinition performed by user"), userId, dataTypeDefinition.Id);
uow.Commit();
-
- uow.Events.Dispatch(Saved, this, new SaveEventArgs(dataTypeDefinition, false));
+ saveEventArgs.CanCancel = false;
+ uow.Events.Dispatch(Saved, this, saveEventArgs);
}
}
@@ -544,7 +554,8 @@ namespace Umbraco.Core.Services
{
using (var uow = UowProvider.GetUnitOfWork())
{
- if (uow.Events.DispatchCancelable(Deleting, this, new DeleteEventArgs(dataTypeDefinition)))
+ var deleteEventArgs = new DeleteEventArgs(dataTypeDefinition);
+ if (uow.Events.DispatchCancelable(Deleting, this, deleteEventArgs))
{
uow.Commit();
return;
@@ -556,8 +567,8 @@ namespace Umbraco.Core.Services
Audit(uow, AuditType.Delete, "Delete DataTypeDefinition performed by user", userId, dataTypeDefinition.Id);
uow.Commit();
-
- uow.Events.Dispatch(Deleted, this, new DeleteEventArgs(dataTypeDefinition, false));
+ deleteEventArgs.CanCancel = false;
+ uow.Events.Dispatch(Deleted, this, deleteEventArgs);
}
}
diff --git a/src/Umbraco.Core/Services/DomainService.cs b/src/Umbraco.Core/Services/DomainService.cs
index d5637d50be..7051f31fe4 100644
--- a/src/Umbraco.Core/Services/DomainService.cs
+++ b/src/Umbraco.Core/Services/DomainService.cs
@@ -32,7 +32,8 @@ namespace Umbraco.Core.Services
using (var uow = UowProvider.GetUnitOfWork())
{
- if (uow.Events.DispatchCancelable(Deleting, this, new DeleteEventArgs(domain, evtMsgs)))
+ var deleteEventArgs = new DeleteEventArgs(domain, evtMsgs);
+ if (uow.Events.DispatchCancelable(Deleting, this, deleteEventArgs))
{
uow.Commit();
return OperationStatus.Cancelled(evtMsgs);
@@ -42,8 +43,8 @@ namespace Umbraco.Core.Services
repository.Delete(domain);
uow.Commit();
- var args = new DeleteEventArgs(domain, false, evtMsgs);
- uow.Events.Dispatch(Deleted, this, args);
+ deleteEventArgs.CanCancel = false;
+ uow.Events.Dispatch(Deleted, this, deleteEventArgs);
return OperationStatus.Success(evtMsgs);
}
@@ -92,7 +93,8 @@ namespace Umbraco.Core.Services
using (var uow = UowProvider.GetUnitOfWork())
{
- if (uow.Events.DispatchCancelable(Saving, this, new SaveEventArgs(domainEntity, evtMsgs)))
+ var saveEventArgs = new SaveEventArgs(domainEntity, evtMsgs);
+ if (uow.Events.DispatchCancelable(Saving, this, saveEventArgs))
{
uow.Commit();
return OperationStatus.Cancelled(evtMsgs);
@@ -101,7 +103,8 @@ namespace Umbraco.Core.Services
var repository = RepositoryFactory.CreateDomainRepository(uow);
repository.AddOrUpdate(domainEntity);
uow.Commit();
- uow.Events.Dispatch(Saved, this, new SaveEventArgs(domainEntity, false, evtMsgs));
+ saveEventArgs.CanCancel = false;
+ uow.Events.Dispatch(Saved, this, saveEventArgs);
return OperationStatus.Success(evtMsgs);
}
diff --git a/src/Umbraco.Core/Services/EntityService.cs b/src/Umbraco.Core/Services/EntityService.cs
index 04d6c276b2..c10311dc01 100644
--- a/src/Umbraco.Core/Services/EntityService.cs
+++ b/src/Umbraco.Core/Services/EntityService.cs
@@ -646,5 +646,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/FileService.cs b/src/Umbraco.Core/Services/FileService.cs
index 4833401df8..916c036a69 100644
--- a/src/Umbraco.Core/Services/FileService.cs
+++ b/src/Umbraco.Core/Services/FileService.cs
@@ -70,7 +70,8 @@ namespace Umbraco.Core.Services
{
using (var uow = UowProvider.GetUnitOfWork())
{
- if (uow.Events.DispatchCancelable(SavingStylesheet, this, new SaveEventArgs(stylesheet)))
+ var saveEventArgs = new SaveEventArgs(stylesheet);
+ if (uow.Events.DispatchCancelable(SavingStylesheet, this, saveEventArgs))
{
uow.Commit();
return;
@@ -78,8 +79,8 @@ namespace Umbraco.Core.Services
var repository = RepositoryFactory.CreateStylesheetRepository(uow);
repository.AddOrUpdate(stylesheet);
-
- uow.Events.Dispatch(SavedStylesheet, this, new SaveEventArgs(stylesheet, false));
+ saveEventArgs.CanCancel = false;
+ uow.Events.Dispatch(SavedStylesheet, this, saveEventArgs);
Audit(uow, AuditType.Save, "Save Stylesheet performed by user", userId, -1);
uow.Commit();
@@ -103,15 +104,16 @@ namespace Umbraco.Core.Services
return;
}
- if (uow.Events.DispatchCancelable(DeletingStylesheet, this, new DeleteEventArgs(stylesheet)))
+ var deleteEventArgs = new DeleteEventArgs(stylesheet);
+ if (uow.Events.DispatchCancelable(DeletingStylesheet, this, deleteEventArgs))
{
uow.Commit();
return;
}
repository.Delete(stylesheet);
-
- uow.Events.Dispatch(DeletedStylesheet, this, new DeleteEventArgs(stylesheet, false));
+ deleteEventArgs.CanCancel = false;
+ uow.Events.Dispatch(DeletedStylesheet, this, deleteEventArgs);
Audit(uow, AuditType.Delete, string.Format("Delete Stylesheet performed by user"), userId, -1);
uow.Commit();
@@ -171,7 +173,8 @@ namespace Umbraco.Core.Services
{
using (var uow = UowProvider.GetUnitOfWork())
{
- if (uow.Events.DispatchCancelable(SavingScript, this, new SaveEventArgs