Compare commits
76 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b8db430615 | |||
| f9d51ba415 | |||
| e07a17db29 | |||
| ad62983327 | |||
| fc8ae1b228 | |||
| 48f27afbc9 | |||
| e1532f9215 | |||
| a2279d03f6 | |||
| e52ff0ea03 | |||
| aaa8e881f8 | |||
| 14ec6b8792 | |||
| 90feb463aa | |||
| 73c4e734b3 | |||
| c85b90d65d | |||
| e923b940f9 | |||
| d29d629eae | |||
| 50b076ce38 | |||
| badb55476b | |||
| 9ecd63eaed | |||
| 5edd61107f | |||
| c21039f1af | |||
| 51f6c639bf | |||
| d9a3fa443d | |||
| 13a1c02507 | |||
| 7a9bc19ffd | |||
| 3dfe653ef4 | |||
| db4dc9006a | |||
| 212244b9f2 | |||
| 12f7492d80 | |||
| d9d68cf6bf | |||
| d94e383ed7 | |||
| 716fc582a0 | |||
| 50c9625cc6 | |||
| 5cbc428590 | |||
| db4d88b2dc | |||
| d9d54f7982 | |||
| a61a2b4d97 | |||
| 735d759208 | |||
| 65f99a9bfd | |||
| 6c23066fb7 | |||
| 717efb6b09 | |||
| 91c52cffc8 | |||
| 03e7779afb | |||
| 15562ec12f | |||
| 6e8862954a | |||
| 9c7ab96c92 | |||
| 49ca4bbc91 | |||
| 5121142760 | |||
| 4c53604bd0 | |||
| bf329aa431 | |||
| d456ee9a31 | |||
| f06d720dbd | |||
| 87839f4f6a | |||
| 02f8c4a106 | |||
| 71ad7df787 | |||
| 4b7520b6f8 | |||
| c06c996ccf | |||
| 9fe4d9c44e | |||
| 560174a21f | |||
| e34a0ec73b | |||
| 152b318d78 | |||
| 414c6b8788 | |||
| 19fb1a55e3 | |||
| b02a9c3e7a | |||
| e6f8309d54 | |||
| 3ecf4144e7 | |||
| 817dd21155 | |||
| 34216fd635 | |||
| c81ff85b41 | |||
| ca1ec0e4a3 | |||
| f2ae41ada2 | |||
| e1d990e6e8 | |||
| ce8b29bfcc | |||
| 1fe1762724 | |||
| 65670ab539 | |||
| 2ab3889b23 |
@@ -25,7 +25,7 @@
|
||||
not want this to happen as the alpha of the next major is, really, the next major already.
|
||||
-->
|
||||
<dependency id="Microsoft.AspNet.SignalR.Core" version="[2.4.0, 2.999999)" />
|
||||
<dependency id="Umbraco.ModelsBuilder.Ui" version="[8.0.0-alpha.38]" />
|
||||
<dependency id="Umbraco.ModelsBuilder.Ui" version="[8.0.1]" />
|
||||
<dependency id="ImageProcessor.Web" version="[4.10.0.100,4.999999)" />
|
||||
<dependency id="ImageProcessor.Web.Config" version="[2.5.0.100,2.999999)" />
|
||||
<dependency id="Microsoft.CodeDom.Providers.DotNetCompilerPlatform" version="[2.0.1,2.999999)" />
|
||||
|
||||
+1
-1
@@ -19,4 +19,4 @@ using System.Resources;
|
||||
|
||||
// these are FYI and changed automatically
|
||||
[assembly: AssemblyFileVersion("8.0.0")]
|
||||
[assembly: AssemblyInformationalVersion("8.0.0-alpha.58")]
|
||||
[assembly: AssemblyInformationalVersion("8.0.0")]
|
||||
|
||||
@@ -79,7 +79,7 @@ namespace Umbraco.Core.Composing.CompositionExtensions
|
||||
composition.RegisterUnique<IFileSystems>(factory => factory.GetInstance<IO.FileSystems>());
|
||||
|
||||
// register the scheme for media paths
|
||||
composition.RegisterUnique<IMediaPathScheme, TwoGuidsMediaPathScheme>();
|
||||
composition.RegisterUnique<IMediaPathScheme, UniqueMediaPathScheme>();
|
||||
|
||||
// register the IMediaFileSystem implementation
|
||||
composition.RegisterFileSystem<IMediaFileSystem, MediaFileSystem>();
|
||||
|
||||
-40
@@ -18,46 +18,6 @@ namespace Umbraco.Core
|
||||
/// </summary>
|
||||
public static partial class CompositionExtensions
|
||||
{
|
||||
#region FileSystems
|
||||
|
||||
/// <summary>
|
||||
/// Registers a filesystem.
|
||||
/// </summary>
|
||||
/// <typeparam name="TFileSystem">The type of the filesystem.</typeparam>
|
||||
/// <typeparam name="TImplementing">The implementing type.</typeparam>
|
||||
/// <param name="composition">The composition.</param>
|
||||
/// <returns>The register.</returns>
|
||||
public static void RegisterFileSystem<TFileSystem, TImplementing>(this Composition composition)
|
||||
where TImplementing : FileSystemWrapper, TFileSystem
|
||||
where TFileSystem : class
|
||||
{
|
||||
composition.RegisterUnique<TFileSystem>(factory =>
|
||||
{
|
||||
var fileSystems = factory.GetInstance<FileSystems>();
|
||||
var supporting = factory.GetInstance<SupportingFileSystems>();
|
||||
return fileSystems.GetFileSystem<TImplementing>(supporting.For<TFileSystem>());
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a filesystem.
|
||||
/// </summary>
|
||||
/// <typeparam name="TFileSystem">The type of the filesystem.</typeparam>
|
||||
/// <param name="composition">The composition.</param>
|
||||
/// <returns>The register.</returns>
|
||||
public static void RegisterFileSystem<TFileSystem>(this Composition composition)
|
||||
where TFileSystem : FileSystemWrapper
|
||||
{
|
||||
composition.RegisterUnique(factory =>
|
||||
{
|
||||
var fileSystems = factory.GetInstance<FileSystems>();
|
||||
var supporting = factory.GetInstance<SupportingFileSystems>();
|
||||
return fileSystems.GetFileSystem<TFileSystem>(supporting.For<TFileSystem>());
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Collection Builders
|
||||
|
||||
/// <summary>
|
||||
@@ -10,8 +10,6 @@ namespace Umbraco.Core
|
||||
/// </summary>
|
||||
public static partial class CompositionExtensions
|
||||
{
|
||||
#region Essentials
|
||||
|
||||
/// <summary>
|
||||
/// Registers essential services.
|
||||
/// </summary>
|
||||
@@ -33,36 +31,5 @@ namespace Umbraco.Core
|
||||
composition.RegisterUnique(typeLoader);
|
||||
composition.RegisterUnique(state);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Unique
|
||||
|
||||
/// <summary>
|
||||
/// Registers a unique service as its own implementation.
|
||||
/// </summary>
|
||||
public static void RegisterUnique<TService>(this Composition composition)
|
||||
=> composition.RegisterUnique(typeof(TService), typeof(TService));
|
||||
|
||||
/// <summary>
|
||||
/// Registers a unique service with an implementation type.
|
||||
/// </summary>
|
||||
public static void RegisterUnique<TService, TImplementing>(this Composition composition)
|
||||
=> composition.RegisterUnique(typeof(TService), typeof(TImplementing));
|
||||
|
||||
/// <summary>
|
||||
/// Registers a unique service with an implementation type, for a target.
|
||||
/// </summary>
|
||||
public static void RegisterUniqueFor<TService, TTarget, TImplementing>(this Composition composition)
|
||||
where TService : class
|
||||
=> composition.RegisterUniqueFor<TService, TTarget>(typeof(TImplementing));
|
||||
|
||||
/// <summary>
|
||||
/// Registers a unique service with an implementing instance.
|
||||
/// </summary>
|
||||
public static void RegisterUnique<TService>(this Composition composition, TService instance)
|
||||
=> composition.RegisterUnique(typeof(TService), instance);
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
using Umbraco.Core.Composing;
|
||||
using Umbraco.Core.IO;
|
||||
|
||||
namespace Umbraco.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides extension methods to the <see cref="Composition"/> class.
|
||||
/// </summary>
|
||||
public static partial class CompositionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers a filesystem.
|
||||
/// </summary>
|
||||
/// <typeparam name="TFileSystem">The type of the filesystem.</typeparam>
|
||||
/// <typeparam name="TImplementing">The implementing type.</typeparam>
|
||||
/// <param name="composition">The composition.</param>
|
||||
/// <returns>The register.</returns>
|
||||
public static void RegisterFileSystem<TFileSystem, TImplementing>(this Composition composition)
|
||||
where TImplementing : FileSystemWrapper, TFileSystem
|
||||
where TFileSystem : class
|
||||
{
|
||||
composition.RegisterUnique<TFileSystem>(factory =>
|
||||
{
|
||||
var fileSystems = factory.GetInstance<FileSystems>();
|
||||
var supporting = factory.GetInstance<SupportingFileSystems>();
|
||||
return fileSystems.GetFileSystem<TImplementing>(supporting.For<TFileSystem>());
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a filesystem.
|
||||
/// </summary>
|
||||
/// <typeparam name="TFileSystem">The type of the filesystem.</typeparam>
|
||||
/// <param name="composition">The composition.</param>
|
||||
/// <returns>The register.</returns>
|
||||
public static void RegisterFileSystem<TFileSystem>(this Composition composition)
|
||||
where TFileSystem : FileSystemWrapper
|
||||
{
|
||||
composition.RegisterUnique(factory =>
|
||||
{
|
||||
var fileSystems = factory.GetInstance<FileSystems>();
|
||||
var supporting = factory.GetInstance<SupportingFileSystems>();
|
||||
return fileSystems.GetFileSystem<TFileSystem>(supporting.For<TFileSystem>());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using Umbraco.Core.Composing;
|
||||
|
||||
namespace Umbraco.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides extension methods to the <see cref="Composition"/> class.
|
||||
/// </summary>
|
||||
public static partial class CompositionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers a unique service as its own implementation.
|
||||
/// </summary>
|
||||
public static void RegisterUnique<TService>(this Composition composition)
|
||||
=> composition.RegisterUnique(typeof(TService), typeof(TService));
|
||||
|
||||
/// <summary>
|
||||
/// Registers a unique service with an implementation type.
|
||||
/// </summary>
|
||||
public static void RegisterUnique<TService, TImplementing>(this Composition composition)
|
||||
=> composition.RegisterUnique(typeof(TService), typeof(TImplementing));
|
||||
|
||||
/// <summary>
|
||||
/// Registers a unique service with an implementation type, for a target.
|
||||
/// </summary>
|
||||
public static void RegisterUniqueFor<TService, TTarget, TImplementing>(this Composition composition)
|
||||
where TService : class
|
||||
=> composition.RegisterUniqueFor<TService, TTarget>(typeof(TImplementing));
|
||||
|
||||
/// <summary>
|
||||
/// Registers a unique service with an implementing instance.
|
||||
/// </summary>
|
||||
public static void RegisterUnique<TService>(this Composition composition, TService instance)
|
||||
=> composition.RegisterUnique(typeof(TService), instance);
|
||||
}
|
||||
}
|
||||
@@ -128,6 +128,7 @@ namespace Umbraco.Core.Configuration
|
||||
if (routes == null) throw new ArgumentNullException(nameof(routes));
|
||||
|
||||
//check if the current request matches a route, if so then it is reserved.
|
||||
//TODO: This value should be cached! Else this is doing double routing in MVC every request!
|
||||
var route = routes.GetRouteData(httpContext);
|
||||
if (route != null)
|
||||
return true;
|
||||
|
||||
@@ -40,5 +40,72 @@ namespace Umbraco.Core
|
||||
|
||||
public DecomposedGuid(Guid value) : this() => this.Value = value;
|
||||
}
|
||||
|
||||
private static readonly char[] Base32Table =
|
||||
{
|
||||
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p',
|
||||
'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5'
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Converts a Guid into a base-32 string.
|
||||
/// </summary>
|
||||
/// <param name="guid">A Guid.</param>
|
||||
/// <param name="length">The string length.</param>
|
||||
/// <returns>A base-32 encoded string.</returns>
|
||||
/// <remarks>
|
||||
/// <para>A base-32 string representation of a Guid is the shortest, efficient, representation
|
||||
/// that is case insensitive (base-64 is case sensitive).</para>
|
||||
/// <para>Length must be 1-26, anything else becomes 26.</para>
|
||||
/// </remarks>
|
||||
public static string ToBase32String(Guid guid, int length = 26)
|
||||
{
|
||||
if (length <= 0 || length > 26)
|
||||
length = 26;
|
||||
|
||||
var bytes = guid.ToByteArray(); // a Guid is 128 bits ie 16 bytes
|
||||
|
||||
// this could be optimized by making it unsafe,
|
||||
// and fixing the table + bytes + chars (see Convert.ToBase64CharArray)
|
||||
|
||||
// each block of 5 bytes = 5*8 = 40 bits
|
||||
// becomes 40 bits = 8*5 = 8 byte-32 chars
|
||||
// a Guid is 3 blocks + 8 bits
|
||||
|
||||
// so it turns into a 3*8+2 = 26 chars string
|
||||
var chars = new char[length];
|
||||
|
||||
var i = 0;
|
||||
var j = 0;
|
||||
|
||||
while (i < 15)
|
||||
{
|
||||
if (j == length) break;
|
||||
chars[j++] = Base32Table[(bytes[i] & 0b1111_1000) >> 3];
|
||||
if (j == length) break;
|
||||
chars[j++] = Base32Table[((bytes[i] & 0b0000_0111) << 2) | ((bytes[i + 1] & 0b1100_0000) >> 6)];
|
||||
if (j == length) break;
|
||||
chars[j++] = Base32Table[(bytes[i + 1] & 0b0011_1110) >> 1];
|
||||
if (j == length) break;
|
||||
chars[j++] = Base32Table[(bytes[i + 1] & 0b0000_0001) | ((bytes[i + 2] & 0b1111_0000) >> 4)];
|
||||
if (j == length) break;
|
||||
chars[j++] = Base32Table[((bytes[i + 2] & 0b0000_1111) << 1) | ((bytes[i + 3] & 0b1000_0000) >> 7)];
|
||||
if (j == length) break;
|
||||
chars[j++] = Base32Table[(bytes[i + 3] & 0b0111_1100) >> 2];
|
||||
if (j == length) break;
|
||||
chars[j++] = Base32Table[((bytes[i + 3] & 0b0000_0011) << 3) | ((bytes[i + 4] & 0b1110_0000) >> 5)];
|
||||
if (j == length) break;
|
||||
chars[j++] = Base32Table[bytes[i + 4] & 0b0001_1111];
|
||||
|
||||
i += 5;
|
||||
}
|
||||
|
||||
if (j < length)
|
||||
chars[j++] = Base32Table[(bytes[i] & 0b1111_1000) >> 3];
|
||||
if (j < length)
|
||||
chars[j] = Base32Table[(bytes[i] & 0b0000_0111) << 2];
|
||||
|
||||
return new string(chars);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace Umbraco.Core.IO
|
||||
// shadow support
|
||||
private readonly List<ShadowWrapper> _shadowWrappers = new List<ShadowWrapper>();
|
||||
private readonly object _shadowLocker = new object();
|
||||
private static Guid _shadowCurrentId = Guid.Empty; // static - unique!!
|
||||
private static string _shadowCurrentId; // static - unique!!
|
||||
#region Constructor
|
||||
|
||||
// DI wants a public ctor
|
||||
@@ -45,13 +45,13 @@ namespace Umbraco.Core.IO
|
||||
_shadowWrappers.Clear();
|
||||
_filesystems.Clear();
|
||||
Volatile.Write(ref _wkfsInitialized, false);
|
||||
_shadowCurrentId = Guid.Empty;
|
||||
_shadowCurrentId = null;
|
||||
}
|
||||
|
||||
// for tests only, totally unsafe
|
||||
internal static void ResetShadowId()
|
||||
{
|
||||
_shadowCurrentId = Guid.Empty;
|
||||
_shadowCurrentId = null;
|
||||
}
|
||||
|
||||
// set by the scope provider when taking control of filesystems
|
||||
@@ -126,11 +126,11 @@ namespace Umbraco.Core.IO
|
||||
var scriptsFileSystem = new PhysicalFileSystem(SystemDirectories.Scripts);
|
||||
var mvcViewsFileSystem = new PhysicalFileSystem(SystemDirectories.MvcViews);
|
||||
|
||||
_macroPartialFileSystem = new ShadowWrapper(macroPartialFileSystem, "Views/MacroPartials", IsScoped);
|
||||
_partialViewsFileSystem = new ShadowWrapper(partialViewsFileSystem, "Views/Partials", IsScoped);
|
||||
_macroPartialFileSystem = new ShadowWrapper(macroPartialFileSystem, "macro-partials", IsScoped);
|
||||
_partialViewsFileSystem = new ShadowWrapper(partialViewsFileSystem, "partials", IsScoped);
|
||||
_stylesheetsFileSystem = new ShadowWrapper(stylesheetsFileSystem, "css", IsScoped);
|
||||
_scriptsFileSystem = new ShadowWrapper(scriptsFileSystem, "scripts", IsScoped);
|
||||
_mvcViewsFileSystem = new ShadowWrapper(mvcViewsFileSystem, "Views", IsScoped);
|
||||
_mvcViewsFileSystem = new ShadowWrapper(mvcViewsFileSystem, "views", IsScoped);
|
||||
|
||||
// TODO: do we need a lock here?
|
||||
_shadowWrappers.Add(_macroPartialFileSystem);
|
||||
@@ -146,6 +146,11 @@ namespace Umbraco.Core.IO
|
||||
|
||||
#region Providers
|
||||
|
||||
private readonly Dictionary<Type, string> _paths = new Dictionary<Type, string>();
|
||||
|
||||
// internal for tests
|
||||
internal IReadOnlyDictionary<Type, string> Paths => _paths;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a strongly-typed filesystem.
|
||||
/// </summary>
|
||||
@@ -162,10 +167,33 @@ namespace Umbraco.Core.IO
|
||||
|
||||
return (TFileSystem) _filesystems.GetOrAdd(typeof(TFileSystem), _ => new Lazy<IFileSystem>(() =>
|
||||
{
|
||||
var name = typeof(TFileSystem).FullName;
|
||||
if (name == null) throw new Exception("panic!");
|
||||
var typeofTFileSystem = typeof(TFileSystem);
|
||||
|
||||
var shadowWrapper = CreateShadowWrapper(supporting, "typed/" + name);
|
||||
// path must be unique and not collide with paths used in CreateWellKnownFileSystems
|
||||
// for our well-known 'media' filesystem we can use the short 'media' path
|
||||
// for others, put them under 'x/' and use ... something
|
||||
string path;
|
||||
if (typeofTFileSystem == typeof(MediaFileSystem))
|
||||
{
|
||||
path = "media";
|
||||
}
|
||||
else
|
||||
{
|
||||
lock (_paths)
|
||||
{
|
||||
if (!_paths.TryGetValue(typeofTFileSystem, out path))
|
||||
{
|
||||
path = Guid.NewGuid().ToString("N").Substring(0, 6);
|
||||
while (_paths.ContainsValue(path)) // this can't loop forever, right?
|
||||
path = Guid.NewGuid().ToString("N").Substring(0, 6);
|
||||
_paths[typeofTFileSystem] = path;
|
||||
}
|
||||
}
|
||||
|
||||
path = "x/" + path;
|
||||
}
|
||||
|
||||
var shadowWrapper = CreateShadowWrapper(supporting, path);
|
||||
return _container.CreateInstance<TFileSystem>(shadowWrapper);
|
||||
})).Value;
|
||||
}
|
||||
@@ -179,35 +207,37 @@ namespace Umbraco.Core.IO
|
||||
// global shadow for the entire application, so great care should be taken to ensure that the
|
||||
// application is *not* doing anything else when using a shadow.
|
||||
|
||||
internal ICompletable Shadow(Guid id)
|
||||
internal ICompletable Shadow()
|
||||
{
|
||||
if (Volatile.Read(ref _wkfsInitialized) == false) EnsureWellKnownFileSystems();
|
||||
|
||||
var id = ShadowWrapper.CreateShadowId();
|
||||
return new ShadowFileSystems(this, id); // will invoke BeginShadow and EndShadow
|
||||
}
|
||||
|
||||
internal void BeginShadow(Guid id)
|
||||
internal void BeginShadow(string id)
|
||||
{
|
||||
lock (_shadowLocker)
|
||||
{
|
||||
// if we throw here, it means that something very wrong happened.
|
||||
if (_shadowCurrentId != Guid.Empty)
|
||||
if (_shadowCurrentId != null)
|
||||
throw new InvalidOperationException("Already shadowing.");
|
||||
|
||||
_shadowCurrentId = id;
|
||||
|
||||
_logger.Debug<ShadowFileSystems>("Shadow '{ShadowId}'", id);
|
||||
_logger.Debug<ShadowFileSystems>("Shadow '{ShadowId}'", _shadowCurrentId);
|
||||
|
||||
foreach (var wrapper in _shadowWrappers)
|
||||
wrapper.Shadow(id);
|
||||
wrapper.Shadow(_shadowCurrentId);
|
||||
}
|
||||
}
|
||||
|
||||
internal void EndShadow(Guid id, bool completed)
|
||||
internal void EndShadow(string id, bool completed)
|
||||
{
|
||||
lock (_shadowLocker)
|
||||
{
|
||||
// if we throw here, it means that something very wrong happened.
|
||||
if (_shadowCurrentId == Guid.Empty)
|
||||
if (_shadowCurrentId == null)
|
||||
throw new InvalidOperationException("Not shadowing.");
|
||||
if (id != _shadowCurrentId)
|
||||
throw new InvalidOperationException("Not the current shadow.");
|
||||
@@ -228,7 +258,7 @@ namespace Umbraco.Core.IO
|
||||
}
|
||||
}
|
||||
|
||||
_shadowCurrentId = Guid.Empty;
|
||||
_shadowCurrentId = null;
|
||||
|
||||
if (exceptions.Count > 0)
|
||||
throw new AggregateException(completed ? "Failed to apply all changes (see exceptions)." : "Failed to abort (see exceptions).", exceptions);
|
||||
@@ -240,7 +270,7 @@ namespace Umbraco.Core.IO
|
||||
lock (_shadowLocker)
|
||||
{
|
||||
var wrapper = new ShadowWrapper(filesystem, shadowPath, IsScoped);
|
||||
if (_shadowCurrentId != Guid.Empty)
|
||||
if (_shadowCurrentId != null)
|
||||
wrapper.Shadow(_shadowCurrentId);
|
||||
_shadowWrappers.Add(wrapper);
|
||||
return wrapper;
|
||||
|
||||
@@ -7,16 +7,21 @@ namespace Umbraco.Core.IO.MediaPathSchemes
|
||||
/// Implements a combined-guids media path scheme.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>Path is "{combinedGuid}/{filename>}" where combinedGuid is a combination of itemGuid and propertyGuid.</para>
|
||||
/// <para>Path is "{combinedGuid}/{filename}" where combinedGuid is a combination of itemGuid and propertyGuid.</para>
|
||||
/// <para>This scheme is dangerous, as it does not prevent potential collisions.</para>
|
||||
/// </remarks>
|
||||
public class CombinedGuidsMediaPathScheme : IMediaPathScheme
|
||||
{
|
||||
private const int DirectoryLength = 8;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetFilePath(IMediaFileSystem fileSystem, Guid itemGuid, Guid propertyGuid, string filename, string previous = null)
|
||||
{
|
||||
// assumes that cuid and puid keys can be trusted - and that a single property type
|
||||
// for a single content cannot store two different files with the same name
|
||||
var directory = HexEncoder.Encode(GuidUtils.Combine(itemGuid, propertyGuid).ToByteArray()/*'/', 2, 4*/); // could use ext to fragment path eg 12/e4/f2/...
|
||||
|
||||
var combinedGuid = GuidUtils.Combine(itemGuid, propertyGuid);
|
||||
var directory = GuidUtils.ToBase32String(combinedGuid, DirectoryLength); // see also HexEncoder, we may want to fragment path eg 12/e4/f3...
|
||||
return Path.Combine(directory, filename).Replace('\\', '/');
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace Umbraco.Core.IO.MediaPathSchemes
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements a unique directory media path scheme.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>This scheme provides short paths, yet handle potential collisions.</para>
|
||||
/// </remarks>
|
||||
public class UniqueMediaPathScheme : IMediaPathScheme
|
||||
{
|
||||
private const int DirectoryLength = 8;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetFilePath(IMediaFileSystem fileSystem, Guid itemGuid, Guid propertyGuid, string filename, string previous = null)
|
||||
{
|
||||
string directory;
|
||||
|
||||
// no point "combining" guids if all we want is some random guid - just get a new one
|
||||
// and then, because we don't want collisions, ensure that the directory does not already exist
|
||||
// (should be quite rare, but eh...)
|
||||
|
||||
do
|
||||
{
|
||||
var combinedGuid = Guid.NewGuid();
|
||||
directory = GuidUtils.ToBase32String(combinedGuid, DirectoryLength); // see also HexEncoder, we may want to fragment path eg 12/e4/f3...
|
||||
|
||||
} while (fileSystem.DirectoryExists(directory));
|
||||
|
||||
return Path.Combine(directory, filename).Replace('\\', '/');
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetDeleteDirectory(IMediaFileSystem fileSystem, string filepath) => Path.GetDirectoryName(filepath);
|
||||
}
|
||||
}
|
||||
@@ -304,7 +304,13 @@ namespace Umbraco.Core.IO
|
||||
// permissions to reach that path, but it may nevertheless be outside of
|
||||
// our root path, due to relative segments, so better check
|
||||
if (IOHelper.PathStartsWith(path, _rootPath, Path.DirectorySeparatorChar))
|
||||
{
|
||||
// this says that 4.7.2 supports long paths - but Windows does not
|
||||
// https://docs.microsoft.com/en-us/dotnet/api/system.io.pathtoolongexception?view=netframework-4.7.2
|
||||
if (path.Length > 260)
|
||||
throw new PathTooLongException($"Path {path} is too long.");
|
||||
return path;
|
||||
}
|
||||
|
||||
// nothing prevents us to reach the file, security-wise, yet it is outside
|
||||
// this filesystem's root - throw
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace Umbraco.Core.IO
|
||||
private bool _completed;
|
||||
|
||||
// invoked by the filesystems when shadowing
|
||||
public ShadowFileSystems(FileSystems fileSystems, Guid id)
|
||||
public ShadowFileSystems(FileSystems fileSystems, string id)
|
||||
{
|
||||
_fileSystems = fileSystems;
|
||||
Id = id;
|
||||
@@ -19,7 +19,7 @@ namespace Umbraco.Core.IO
|
||||
}
|
||||
|
||||
// for tests
|
||||
public Guid Id { get; }
|
||||
public string Id { get; }
|
||||
|
||||
// invoked by the scope when exiting, if completed
|
||||
public void Complete()
|
||||
|
||||
@@ -22,7 +22,33 @@ namespace Umbraco.Core.IO
|
||||
_isScoped = isScoped;
|
||||
}
|
||||
|
||||
internal void Shadow(Guid id)
|
||||
public static string CreateShadowId()
|
||||
{
|
||||
const int retries = 50; // avoid infinite loop
|
||||
const int idLength = 8; // 6 chars
|
||||
|
||||
// shorten a Guid to idLength chars, and see whether it collides
|
||||
// with an existing directory or not - if it does, try again, and
|
||||
// we should end up with a unique identifier eventually - but just
|
||||
// detect infinite loops (just in case)
|
||||
|
||||
for (var i = 0; i < retries; i++)
|
||||
{
|
||||
var id = GuidUtils.ToBase32String(Guid.NewGuid(), idLength);
|
||||
|
||||
var virt = ShadowFsPath + "/" + id;
|
||||
var shadowDir = IOHelper.MapPath(virt);
|
||||
if (Directory.Exists(shadowDir))
|
||||
continue;
|
||||
|
||||
Directory.CreateDirectory(shadowDir);
|
||||
return id;
|
||||
}
|
||||
|
||||
throw new Exception($"Could not get a shadow identifier (tried {retries} times)");
|
||||
}
|
||||
|
||||
internal void Shadow(string id)
|
||||
{
|
||||
// note: no thread-safety here, because ShadowFs is thread-safe due to the check
|
||||
// on ShadowFileSystemsScope.None - and if None is false then we should be running
|
||||
|
||||
@@ -281,7 +281,7 @@ namespace Umbraco.Core.Migrations.Install
|
||||
_database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -37, EditorAlias = Constants.PropertyEditors.Aliases.ColorPicker, DbType = "Nvarchar" });
|
||||
InsertDataTypeDto(Constants.DataTypes.DropDownSingle, Constants.PropertyEditors.Aliases.DropDownListFlexible, "Nvarchar", "{\"multiple\":false}");
|
||||
_database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -40, EditorAlias = Constants.PropertyEditors.Aliases.RadioButtonList, DbType = "Nvarchar" });
|
||||
_database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -41, EditorAlias = "Umbraco.Date", DbType = "Date" });
|
||||
_database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -41, EditorAlias = "Umbraco.DateTime", DbType = "Date", Configuration = "{\"format\":\"YYYY-MM-DD\"}" });
|
||||
InsertDataTypeDto(Constants.DataTypes.DropDownMultiple, Constants.PropertyEditors.Aliases.DropDownListFlexible, "Nvarchar", "{\"multiple\":true}");
|
||||
_database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -43, EditorAlias = Constants.PropertyEditors.Aliases.CheckBoxList, DbType = "Nvarchar" });
|
||||
_database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = 1041, EditorAlias = Constants.PropertyEditors.Aliases.Tags, DbType = "Ntext",
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Umbraco.Core.Collections;
|
||||
using Umbraco.Core.Composing;
|
||||
using Umbraco.Core.Exceptions;
|
||||
using Umbraco.Core.PropertyEditors;
|
||||
using Umbraco.Core.Services;
|
||||
|
||||
namespace Umbraco.Core.Models
|
||||
{
|
||||
@@ -101,35 +105,6 @@ namespace Umbraco.Core.Models
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the content item's properties pass variant rules
|
||||
/// </summary>
|
||||
/// <para>If the content type is variant, then culture can be either '*' or an actual culture, but neither 'null' nor
|
||||
/// 'empty'. If the content type is invariant, then culture can be either '*' or null or empty.</para>
|
||||
public static Property[] ValidateProperties(this IContentBase content, string culture = "*")
|
||||
{
|
||||
// select invalid properties
|
||||
return content.Properties.Where(x =>
|
||||
{
|
||||
// if culture is null, we validate invariant properties only
|
||||
// if culture is '*' we validate both variant and invariant properties, automatically
|
||||
// if culture is specific eg 'en-US' we both too, but explicitly
|
||||
|
||||
var varies = x.PropertyType.VariesByCulture();
|
||||
|
||||
if (culture == null)
|
||||
return !(varies || x.IsValid(null)); // validate invariant property, invariant culture
|
||||
|
||||
if (culture == "*")
|
||||
return !x.IsValid(culture); // validate property, all cultures
|
||||
|
||||
return varies
|
||||
? !x.IsValid(culture) // validate variant property, explicit culture
|
||||
: !x.IsValid(null); // validate invariant property, explicit culture
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public static void SetPublishInfo(this IContent content, string culture, string name, DateTime date)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
@@ -191,6 +166,13 @@ namespace Umbraco.Core.Models
|
||||
content.CultureInfos.AddOrUpdate(culture, name, date);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the publishing values for names and properties.
|
||||
/// </summary>
|
||||
/// <param name="content"></param>
|
||||
/// <param name="culture"></param>
|
||||
/// <returns>A value indicating whether it was possible to publish the names and values for the specified
|
||||
/// culture(s). The method may fail if required names are not set, but it does NOT validate property data</returns>
|
||||
public static bool PublishCulture(this IContent content, string culture = "*")
|
||||
{
|
||||
culture = culture.NullOrWhiteSpaceAsNull();
|
||||
@@ -201,10 +183,6 @@ namespace Umbraco.Core.Models
|
||||
if (!content.ContentType.SupportsPropertyVariation(culture, "*", true))
|
||||
throw new NotSupportedException($"Culture \"{culture}\" is not supported by content type \"{content.ContentType.Alias}\" with variation \"{content.ContentType.Variations}\".");
|
||||
|
||||
// the values we want to publish should be valid
|
||||
if (content.ValidateProperties(culture).Any())
|
||||
return false;
|
||||
|
||||
var alsoInvariant = false;
|
||||
if (culture == "*") // all cultures
|
||||
{
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Umbraco.Core.Models
|
||||
[DataContract(IsReference = true)]
|
||||
public class ContentType : ContentTypeCompositionBase, IContentType
|
||||
{
|
||||
public const bool IsPublishingConst = true;
|
||||
public const bool SupportsPublishingConst = true;
|
||||
|
||||
private int _defaultTemplate;
|
||||
private IEnumerable<ITemplate> _allowedTemplates;
|
||||
@@ -45,7 +45,7 @@ namespace Umbraco.Core.Models
|
||||
public override ISimpleContentType ToSimple() => new SimpleContentType(this);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool IsPublishing => IsPublishingConst;
|
||||
public override bool SupportsPublishing => SupportsPublishingConst;
|
||||
|
||||
//Custom comparer for enumerable
|
||||
private static readonly DelegateEqualityComparer<IEnumerable<ITemplate>> TemplateComparer = new DelegateEqualityComparer<IEnumerable<ITemplate>>(
|
||||
|
||||
@@ -40,7 +40,7 @@ namespace Umbraco.Core.Models
|
||||
|
||||
// actually OK as IsPublishing is constant
|
||||
// ReSharper disable once VirtualMemberCallInConstructor
|
||||
_noGroupPropertyTypes = new PropertyTypeCollection(IsPublishing);
|
||||
_noGroupPropertyTypes = new PropertyTypeCollection(SupportsPublishing);
|
||||
_noGroupPropertyTypes.CollectionChanged += PropertyTypesChanged;
|
||||
|
||||
_variations = ContentVariation.Nothing;
|
||||
@@ -61,7 +61,7 @@ namespace Umbraco.Core.Models
|
||||
|
||||
// actually OK as IsPublishing is constant
|
||||
// ReSharper disable once VirtualMemberCallInConstructor
|
||||
_noGroupPropertyTypes = new PropertyTypeCollection(IsPublishing);
|
||||
_noGroupPropertyTypes = new PropertyTypeCollection(SupportsPublishing);
|
||||
_noGroupPropertyTypes.CollectionChanged += PropertyTypesChanged;
|
||||
|
||||
_variations = ContentVariation.Nothing;
|
||||
@@ -70,7 +70,7 @@ namespace Umbraco.Core.Models
|
||||
public abstract ISimpleContentType ToSimple();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the content type is publishing.
|
||||
/// Gets a value indicating whether the content type supports publishing.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>A publishing content type supports draft and published values for properties.
|
||||
@@ -80,7 +80,7 @@ namespace Umbraco.Core.Models
|
||||
/// the draft or published value of a property returns the same thing, and publishing
|
||||
/// a value property has no effect.</para>
|
||||
/// </remarks>
|
||||
public abstract bool IsPublishing { get; }
|
||||
public abstract bool SupportsPublishing { get; }
|
||||
|
||||
//Custom comparer for enumerable
|
||||
private static readonly DelegateEqualityComparer<IEnumerable<ContentTypeSort>> ContentTypeSortComparer =
|
||||
@@ -257,7 +257,7 @@ namespace Umbraco.Core.Models
|
||||
{
|
||||
if (_noGroupPropertyTypes != null)
|
||||
_noGroupPropertyTypes.CollectionChanged -= PropertyTypesChanged;
|
||||
_noGroupPropertyTypes = new PropertyTypeCollection(IsPublishing, value);
|
||||
_noGroupPropertyTypes = new PropertyTypeCollection(SupportsPublishing, value);
|
||||
_noGroupPropertyTypes.CollectionChanged += PropertyTypesChanged;
|
||||
PropertyTypesChanged(_noGroupPropertyTypes, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
||||
}
|
||||
|
||||
@@ -226,7 +226,7 @@ namespace Umbraco.Core.Models
|
||||
return null;
|
||||
|
||||
// create the new group
|
||||
var group = new PropertyGroup(IsPublishing) { Name = name, SortOrder = 0 };
|
||||
var group = new PropertyGroup(SupportsPublishing) { Name = name, SortOrder = 0 };
|
||||
|
||||
// check if it is inherited - there might be more than 1 but we want the 1st, to
|
||||
// reuse its sort order - if there are more than 1 and they have different sort
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace Umbraco.Core.Models
|
||||
[DataContract(IsReference = true)]
|
||||
public class MediaType : ContentTypeCompositionBase, IMediaType
|
||||
{
|
||||
public const bool IsPublishingConst = false;
|
||||
public const bool SupportsPublishingConst = false;
|
||||
|
||||
/// <summary>
|
||||
/// Constuctor for creating a MediaType with the parent's id.
|
||||
@@ -45,7 +45,7 @@ namespace Umbraco.Core.Models
|
||||
public override ISimpleContentType ToSimple() => new SimpleContentType(this);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool IsPublishing => IsPublishingConst;
|
||||
public override bool SupportsPublishing => SupportsPublishingConst;
|
||||
|
||||
/// <inheritdoc />
|
||||
IMediaType IMediaType.DeepCloneWithResetIdentities(string newAlias) => (IMediaType)DeepCloneWithResetIdentities(newAlias);
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Umbraco.Core.Models
|
||||
[DataContract(IsReference = true)]
|
||||
public class MemberType : ContentTypeCompositionBase, IMemberType
|
||||
{
|
||||
public const bool IsPublishingConst = false;
|
||||
public const bool SupportsPublishingConst = false;
|
||||
|
||||
//Dictionary is divided into string: PropertyTypeAlias, Tuple: MemberCanEdit, VisibleOnProfile, PropertyTypeId
|
||||
private string _alias;
|
||||
@@ -35,7 +35,7 @@ namespace Umbraco.Core.Models
|
||||
public override ISimpleContentType ToSimple() => new SimpleContentType(this);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool IsPublishing => IsPublishingConst;
|
||||
public override bool SupportsPublishing => SupportsPublishingConst;
|
||||
|
||||
public override ContentVariation Variations
|
||||
{
|
||||
|
||||
@@ -184,7 +184,7 @@ namespace Umbraco.Core.Models
|
||||
{
|
||||
if (pvalue == null) return null;
|
||||
|
||||
return PropertyType.IsPublishing
|
||||
return PropertyType.SupportsPublishing
|
||||
? (published ? pvalue.PublishedValue : pvalue.EditedValue)
|
||||
: pvalue.EditedValue;
|
||||
}
|
||||
@@ -244,7 +244,7 @@ namespace Umbraco.Core.Models
|
||||
{
|
||||
if (pvalue == null) return;
|
||||
|
||||
if (!PropertyType.IsPublishing)
|
||||
if (!PropertyType.SupportsPublishing)
|
||||
throw new NotSupportedException("Property type does not support publishing.");
|
||||
var origValue = pvalue.PublishedValue;
|
||||
pvalue.PublishedValue = PropertyType.ConvertAssignedValue(pvalue.EditedValue);
|
||||
@@ -255,7 +255,7 @@ namespace Umbraco.Core.Models
|
||||
{
|
||||
if (pvalue == null) return;
|
||||
|
||||
if (!PropertyType.IsPublishing)
|
||||
if (!PropertyType.SupportsPublishing)
|
||||
throw new NotSupportedException("Property type does not support publishing.");
|
||||
var origValue = pvalue.PublishedValue;
|
||||
pvalue.PublishedValue = PropertyType.ConvertAssignedValue(null);
|
||||
@@ -288,7 +288,7 @@ namespace Umbraco.Core.Models
|
||||
{
|
||||
var (pvalue, _) = GetPValue(culture, segment, true);
|
||||
|
||||
if (published && PropertyType.IsPublishing)
|
||||
if (published && PropertyType.SupportsPublishing)
|
||||
pvalue.PublishedValue = value;
|
||||
else
|
||||
pvalue.EditedValue = value;
|
||||
@@ -332,56 +332,6 @@ namespace Umbraco.Core.Models
|
||||
return (pvalue, change);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the property has valid values.
|
||||
/// </summary>
|
||||
internal bool IsValid(string culture = "*", string segment = "*")
|
||||
{
|
||||
// TODO: validating values shouldn't be done here, this calls in to IsValidValue
|
||||
|
||||
culture = culture.NullOrWhiteSpaceAsNull();
|
||||
segment = segment.NullOrWhiteSpaceAsNull();
|
||||
|
||||
// if validating invariant/neutral, and it is supported, validate
|
||||
// (including ensuring that the value exists, if mandatory)
|
||||
if ((culture == null || culture == "*") && (segment == null || segment == "*") && PropertyType.SupportsVariation(null, null))
|
||||
if (!IsValidValue(_pvalue?.EditedValue))
|
||||
return false;
|
||||
|
||||
// if validating only invariant/neutral, we are good
|
||||
if (culture == null && segment == null)
|
||||
return true;
|
||||
|
||||
// if nothing else to validate, we are good
|
||||
if ((culture == null || culture == "*") && (segment == null || segment == "*") && !PropertyType.VariesByCulture())
|
||||
return true;
|
||||
|
||||
// for anything else, validate the existing values (including mandatory),
|
||||
// but we cannot validate mandatory globally (we don't know the possible cultures and segments)
|
||||
|
||||
if (_vvalues == null) return culture == "*" || IsValidValue(null);
|
||||
|
||||
var pvalues = _vvalues.Where(x =>
|
||||
PropertyType.SupportsVariation(x.Value.Culture, x.Value.Segment, true) && // the value variation is ok
|
||||
(culture == "*" || x.Value.Culture.InvariantEquals(culture)) && // the culture matches
|
||||
(segment == "*" || x.Value.Segment.InvariantEquals(segment))) // the segment matches
|
||||
.Select(x => x.Value)
|
||||
.ToList();
|
||||
|
||||
return pvalues.Count == 0 || pvalues.All(x => IsValidValue(x.EditedValue));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Boolean indicating whether the passed in value is valid
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
/// <returns>True is property value is valid, otherwise false</returns>
|
||||
private bool IsValidValue(object value)
|
||||
{
|
||||
// TODO: this shouldn't exist here, the model itself shouldn't be responsible for it's own validation and this requires singleton access
|
||||
return PropertyType.IsPropertyValueValid(value);
|
||||
}
|
||||
|
||||
protected override void PerformDeepClone(object clone)
|
||||
{
|
||||
base.PerformDeepClone(clone);
|
||||
|
||||
@@ -82,9 +82,22 @@ namespace Umbraco.Core.Models
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the content type, owning this property type, is publishing.
|
||||
/// Gets a value indicating whether the content type owning this property type is publishing.
|
||||
/// </summary>
|
||||
public bool IsPublishing { get; internal set; }
|
||||
/// <remarks>
|
||||
/// <para>A publishing content type supports draft and published values for properties.
|
||||
/// It is possible to retrieve either the draft (default) or published value of a property.
|
||||
/// Setting the value always sets the draft value, which then needs to be published.</para>
|
||||
/// <para>A non-publishing content type only supports one value for properties. Getting
|
||||
/// the draft or published value of a property returns the same thing, and publishing
|
||||
/// a value property has no effect.</para>
|
||||
/// <para>When true, getting the property value returns the edited value by default, but
|
||||
/// it is possible to get the published value using the appropriate 'published' method
|
||||
/// parameter.</para>
|
||||
/// <para>When false, getting the property value always return the edited value,
|
||||
/// regardless of the 'published' method parameter.</para>
|
||||
/// </remarks>
|
||||
public bool SupportsPublishing { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets of sets the name of the property type.
|
||||
@@ -355,18 +368,6 @@ namespace Umbraco.Core.Models
|
||||
}
|
||||
|
||||
|
||||
// TODO: this and other value validation methods should be a service level (not a model) thing. Changing this to internal for now
|
||||
/// <summary>
|
||||
/// Determines whether a value is valid for this property type.
|
||||
/// </summary>
|
||||
internal bool IsPropertyValueValid(object value)
|
||||
{
|
||||
var editor = Current.PropertyEditors[_propertyEditorAlias]; // TODO: inject
|
||||
var configuration = Current.Services.DataTypeService.GetDataType(_dataTypeId).Configuration; // TODO: inject
|
||||
var valueEditor = editor.GetValueEditor(configuration);
|
||||
return !valueEditor.Validate(value, Mandatory, ValidationRegExp).Any();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sanitizes a property type alias.
|
||||
/// </summary>
|
||||
|
||||
@@ -23,18 +23,18 @@ namespace Umbraco.Core.Models
|
||||
[IgnoreDataMember]
|
||||
internal Action OnAdd;
|
||||
|
||||
internal PropertyTypeCollection(bool isPublishing)
|
||||
internal PropertyTypeCollection(bool supportsPublishing)
|
||||
{
|
||||
IsPublishing = isPublishing;
|
||||
SupportsPublishing = supportsPublishing;
|
||||
}
|
||||
|
||||
public PropertyTypeCollection(bool isPublishing, IEnumerable<PropertyType> properties)
|
||||
: this(isPublishing)
|
||||
public PropertyTypeCollection(bool supportsPublishing, IEnumerable<PropertyType> properties)
|
||||
: this(supportsPublishing)
|
||||
{
|
||||
Reset(properties);
|
||||
}
|
||||
|
||||
public bool IsPublishing { get; }
|
||||
public bool SupportsPublishing { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Resets the collection to only contain the <see cref="PropertyType"/> instances referenced in the <paramref name="properties"/> parameter.
|
||||
@@ -51,7 +51,7 @@ namespace Umbraco.Core.Models
|
||||
|
||||
protected override void SetItem(int index, PropertyType item)
|
||||
{
|
||||
item.IsPublishing = IsPublishing;
|
||||
item.SupportsPublishing = SupportsPublishing;
|
||||
base.SetItem(index, item);
|
||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index));
|
||||
}
|
||||
@@ -65,7 +65,7 @@ namespace Umbraco.Core.Models
|
||||
|
||||
protected override void InsertItem(int index, PropertyType item)
|
||||
{
|
||||
item.IsPublishing = IsPublishing;
|
||||
item.SupportsPublishing = SupportsPublishing;
|
||||
base.InsertItem(index, item);
|
||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item));
|
||||
}
|
||||
@@ -79,7 +79,7 @@ namespace Umbraco.Core.Models
|
||||
// TODO: Instead of 'new' this should explicitly implement one of the collection interfaces members
|
||||
internal new void Add(PropertyType item)
|
||||
{
|
||||
item.IsPublishing = IsPublishing;
|
||||
item.SupportsPublishing = SupportsPublishing;
|
||||
|
||||
// TODO: this is not pretty and should be refactored
|
||||
try
|
||||
@@ -155,7 +155,7 @@ namespace Umbraco.Core.Models
|
||||
|
||||
public object DeepClone()
|
||||
{
|
||||
var clone = new PropertyTypeCollection(IsPublishing);
|
||||
var clone = new PropertyTypeCollection(SupportsPublishing);
|
||||
foreach (var propertyType in this)
|
||||
clone.Add((PropertyType) propertyType.DeepClone());
|
||||
return clone;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace Umbraco.Core.Models.PublishedContent
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a live published model creation service.
|
||||
/// </summary>
|
||||
public interface ILivePublishedModelFactory : IPublishedModelFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets an object that can be used to synchronize access to the factory.
|
||||
/// </summary>
|
||||
object SyncRoot { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the factory.
|
||||
/// </summary>
|
||||
void Refresh();
|
||||
}
|
||||
}
|
||||
@@ -78,7 +78,7 @@ namespace Umbraco.Core.Persistence.Factories
|
||||
var propertyGroups = new PropertyGroupCollection();
|
||||
foreach (var groupDto in dto.PropertyTypeGroups.Where(x => x.Id.HasValue))
|
||||
{
|
||||
var group = new PropertyGroup(MemberType.IsPublishingConst);
|
||||
var group = new PropertyGroup(MemberType.SupportsPublishingConst);
|
||||
|
||||
// if the group is defined on the current member type,
|
||||
// assign its identifier, else it will be zero
|
||||
@@ -93,7 +93,7 @@ namespace Umbraco.Core.Persistence.Factories
|
||||
group.Key = groupDto.UniqueId;
|
||||
group.Name = groupDto.Text;
|
||||
group.SortOrder = groupDto.SortOrder;
|
||||
group.PropertyTypes = new PropertyTypeCollection(MemberType.IsPublishingConst);
|
||||
group.PropertyTypes = new PropertyTypeCollection(MemberType.SupportsPublishingConst);
|
||||
|
||||
//Because we are likely to have a group with no PropertyTypes we need to ensure that these are excluded
|
||||
var localGroupDto = groupDto;
|
||||
|
||||
@@ -121,7 +121,7 @@ namespace Umbraco.Core.Persistence.Factories
|
||||
|
||||
foreach (var property in properties)
|
||||
{
|
||||
if (property.PropertyType.IsPublishing)
|
||||
if (property.PropertyType.SupportsPublishing)
|
||||
{
|
||||
//create the resulting hashset if it's not created and the entity varies by culture
|
||||
if (entityVariesByCulture && editedCultures == null)
|
||||
|
||||
@@ -4,10 +4,7 @@ using System.Data;
|
||||
using System.Data.SqlClient;
|
||||
using System.Data.SqlServerCe;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using NPoco;
|
||||
using StackExchange.Profiling.Data;
|
||||
using Umbraco.Core.Persistence.SqlSyntax;
|
||||
|
||||
namespace Umbraco.Core.Persistence
|
||||
|
||||
@@ -147,8 +147,23 @@ namespace Umbraco.Core.Persistence
|
||||
private static TConnection GetTypedConnection<TConnection>(IDbConnection connection)
|
||||
where TConnection : class, IDbConnection
|
||||
{
|
||||
var profiled = connection as ProfiledDbConnection;
|
||||
return profiled == null ? connection as TConnection : profiled.WrappedConnection as TConnection;
|
||||
var c = connection;
|
||||
for (;;)
|
||||
{
|
||||
switch (c)
|
||||
{
|
||||
case TConnection ofType:
|
||||
return ofType;
|
||||
case RetryDbConnection retry:
|
||||
c = retry.Inner;
|
||||
break;
|
||||
case ProfiledDbConnection profiled:
|
||||
c = profiled.WrappedConnection;
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException(connection.GetType().FullName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -160,8 +175,20 @@ namespace Umbraco.Core.Persistence
|
||||
private static TTransaction GetTypedTransaction<TTransaction>(IDbTransaction transaction)
|
||||
where TTransaction : class, IDbTransaction
|
||||
{
|
||||
var profiled = transaction as ProfiledDbTransaction;
|
||||
return profiled == null ? transaction as TTransaction : profiled.WrappedTransaction as TTransaction;
|
||||
var t = transaction;
|
||||
for (;;)
|
||||
{
|
||||
switch (t)
|
||||
{
|
||||
case TTransaction ofType:
|
||||
return ofType;
|
||||
case ProfiledDbTransaction profiled:
|
||||
t = profiled.WrappedTransaction;
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException(transaction.GetType().FullName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -173,11 +200,23 @@ namespace Umbraco.Core.Persistence
|
||||
private static TCommand GetTypedCommand<TCommand>(IDbCommand command)
|
||||
where TCommand : class, IDbCommand
|
||||
{
|
||||
var faultHandling = command as FaultHandlingDbCommand;
|
||||
if (faultHandling != null) command = faultHandling.Inner;
|
||||
var profiled = command as ProfiledDbCommand;
|
||||
if (profiled != null) command = profiled.InternalCommand;
|
||||
return command as TCommand;
|
||||
var c = command;
|
||||
for (;;)
|
||||
{
|
||||
switch (c)
|
||||
{
|
||||
case TCommand ofType:
|
||||
return ofType;
|
||||
case FaultHandlingDbCommand faultHandling:
|
||||
c = faultHandling.Inner;
|
||||
break;
|
||||
case ProfiledDbCommand profiled:
|
||||
c = profiled.InternalCommand;
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException(command.GetType().FullName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void TruncateTable(this IDatabase db, ISqlSyntaxProvider sqlSyntax, string tableName)
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using NPoco;
|
||||
using Umbraco.Core.Persistence.Querying;
|
||||
|
||||
@@ -1119,6 +1120,25 @@ namespace Umbraco.Core.Persistence
|
||||
|
||||
#endregion
|
||||
|
||||
#region Aliasing
|
||||
|
||||
internal static string GetAliasedField(this Sql<ISqlContext> sql, string field)
|
||||
{
|
||||
// get alias, if aliased
|
||||
//
|
||||
// regex looks for pattern "([\w+].[\w+]) AS ([\w+])" ie "(field) AS (alias)"
|
||||
// and, if found & a group's field matches the field name, returns the alias
|
||||
//
|
||||
// so... if query contains "[umbracoNode].[nodeId] AS [umbracoNode__nodeId]"
|
||||
// then GetAliased for "[umbracoNode].[nodeId]" returns "[umbracoNode__nodeId]"
|
||||
|
||||
var matches = sql.SqlContext.SqlSyntax.AliasRegex.Matches(sql.SQL);
|
||||
var match = matches.Cast<Match>().FirstOrDefault(m => m.Groups[1].Value.InvariantEquals(field));
|
||||
return match == null ? field : match.Groups[2].Value;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Utilities
|
||||
|
||||
private static string[] GetColumns<TDto>(this Sql<ISqlContext> sql, string tableAlias = null, string referenceName = null, Expression<Func<TDto, object>>[] columnExpressions = null, bool withAlias = true)
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
|
||||
_templateRepository = templateRepository;
|
||||
}
|
||||
|
||||
protected override bool IsPublishing => ContentType.IsPublishingConst;
|
||||
protected override bool SupportsPublishing => ContentType.SupportsPublishingConst;
|
||||
|
||||
protected override IRepositoryCachePolicy<IContentType, int> CreateCachePolicy()
|
||||
{
|
||||
@@ -60,11 +60,11 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
|
||||
if (ids.Any())
|
||||
{
|
||||
//NOTE: This logic should never be executed according to our cache policy
|
||||
return ContentTypeQueryMapper.GetContentTypes(Database, SqlSyntax, IsPublishing, this, _templateRepository)
|
||||
return ContentTypeQueryMapper.GetContentTypes(Database, SqlSyntax, SupportsPublishing, this, _templateRepository)
|
||||
.Where(x => ids.Contains(x.Id));
|
||||
}
|
||||
|
||||
return ContentTypeQueryMapper.GetContentTypes(Database, SqlSyntax, IsPublishing, this, _templateRepository);
|
||||
return ContentTypeQueryMapper.GetContentTypes(Database, SqlSyntax, SupportsPublishing, this, _templateRepository);
|
||||
}
|
||||
|
||||
protected override IEnumerable<IContentType> PerformGetAll(params Guid[] ids)
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
|
||||
: base(scopeAccessor, cache, logger)
|
||||
{ }
|
||||
|
||||
protected abstract bool IsPublishing { get; }
|
||||
protected abstract bool SupportsPublishing { get; }
|
||||
|
||||
public IEnumerable<MoveEventInfo<TEntity>> Move(TEntity moving, EntityContainer container)
|
||||
{
|
||||
@@ -1021,7 +1021,7 @@ AND umbracoNode.id <> @id",
|
||||
var dtos = Database
|
||||
.Fetch<PropertyTypeGroupDto>(sql);
|
||||
|
||||
var propertyGroups = PropertyGroupFactory.BuildEntity(dtos, IsPublishing, id, createDate, updateDate,CreatePropertyType);
|
||||
var propertyGroups = PropertyGroupFactory.BuildEntity(dtos, SupportsPublishing, id, createDate, updateDate,CreatePropertyType);
|
||||
|
||||
return new PropertyGroupCollection(propertyGroups);
|
||||
}
|
||||
@@ -1057,7 +1057,7 @@ AND umbracoNode.id <> @id",
|
||||
//Reset dirty properties
|
||||
Parallel.ForEach(list, currentFile => currentFile.ResetDirtyProperties(false));
|
||||
|
||||
return new PropertyTypeCollection(IsPublishing, list);
|
||||
return new PropertyTypeCollection(SupportsPublishing, list);
|
||||
}
|
||||
|
||||
protected void ValidateAlias(PropertyType pt)
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
|
||||
: base(scopeAccessor, cache, logger)
|
||||
{ }
|
||||
|
||||
protected override bool IsPublishing => MediaType.IsPublishingConst;
|
||||
protected override bool SupportsPublishing => MediaType.SupportsPublishingConst;
|
||||
|
||||
protected override IRepositoryCachePolicy<IMediaType, int> CreateCachePolicy()
|
||||
{
|
||||
@@ -55,11 +55,11 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
|
||||
if (ids.Any())
|
||||
{
|
||||
//NOTE: This logic should never be executed according to our cache policy
|
||||
return ContentTypeQueryMapper.GetMediaTypes(Database, SqlSyntax, IsPublishing, this)
|
||||
return ContentTypeQueryMapper.GetMediaTypes(Database, SqlSyntax, SupportsPublishing, this)
|
||||
.Where(x => ids.Contains(x.Id));
|
||||
}
|
||||
|
||||
return ContentTypeQueryMapper.GetMediaTypes(Database, SqlSyntax, IsPublishing, this);
|
||||
return ContentTypeQueryMapper.GetMediaTypes(Database, SqlSyntax, SupportsPublishing, this);
|
||||
}
|
||||
|
||||
protected override IEnumerable<IMediaType> PerformGetAll(params Guid[] ids)
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
|
||||
: base(scopeAccessor, cache, logger)
|
||||
{ }
|
||||
|
||||
protected override bool IsPublishing => MemberType.IsPublishingConst;
|
||||
protected override bool SupportsPublishing => MemberType.SupportsPublishingConst;
|
||||
|
||||
protected override IRepositoryCachePolicy<IMemberType, int> CreateCachePolicy()
|
||||
{
|
||||
|
||||
@@ -702,23 +702,6 @@ ORDER BY colName";
|
||||
{
|
||||
if (orderBy == null) throw new ArgumentNullException(nameof(orderBy));
|
||||
|
||||
// get the referenced column name and find the corresp mapped column name
|
||||
var expressionMember = ExpressionHelper.GetMemberInfo(orderBy);
|
||||
var mapper = _mapperCollection[typeof(IUser)];
|
||||
var mappedField = mapper.Map(SqlContext.SqlSyntax, expressionMember.Name);
|
||||
|
||||
if (mappedField.IsNullOrWhiteSpace())
|
||||
throw new ArgumentException("Could not find a mapping for the column specified in the orderBy clause");
|
||||
|
||||
return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, mappedField, orderDirection, includeUserGroups, excludeUserGroups, userState, filter);
|
||||
}
|
||||
|
||||
private IEnumerable<IUser> GetPagedResultsByQuery(IQuery<IUser> query, long pageIndex, int pageSize, out long totalRecords,
|
||||
string orderBy, Direction orderDirection = Direction.Ascending,
|
||||
string[] includeUserGroups = null, string[] excludeUserGroups = null, UserState[] userState = null, IQuery<IUser> filter = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(orderBy)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(orderBy));
|
||||
|
||||
Sql<ISqlContext> filterSql = null;
|
||||
var customFilterWheres = filter?.GetWhereClauses().ToArray();
|
||||
var hasCustomFilter = customFilterWheres != null && customFilterWheres.Length > 0;
|
||||
@@ -734,7 +717,6 @@ ORDER BY colName";
|
||||
filterSql.Append($"AND ({clause.Item1})", clause.Item2);
|
||||
}
|
||||
|
||||
|
||||
if (includeUserGroups != null && includeUserGroups.Length > 0)
|
||||
{
|
||||
const string subQuery = @"AND (umbracoUser.id IN (SELECT DISTINCT umbracoUser.id
|
||||
@@ -808,7 +790,7 @@ ORDER BY colName";
|
||||
sql = new SqlTranslator<IUser>(sql, query).Translate();
|
||||
|
||||
// get sorted and filtered sql
|
||||
var sqlNodeIdsWithSort = ApplySort(ApplyFilter(sql, filterSql, query != null), orderDirection, orderBy);
|
||||
var sqlNodeIdsWithSort = ApplySort(ApplyFilter(sql, filterSql, query != null), orderBy, orderDirection);
|
||||
|
||||
// get a page of results and total count
|
||||
var pagedResult = Database.Page<UserDto>(pageIndex + 1, pageSize, sqlNodeIdsWithSort);
|
||||
@@ -834,14 +816,36 @@ ORDER BY colName";
|
||||
return sql;
|
||||
}
|
||||
|
||||
private static Sql<ISqlContext> ApplySort(Sql<ISqlContext> sql, Direction orderDirection, string orderBy)
|
||||
private Sql<ISqlContext> ApplySort(Sql<ISqlContext> sql, Expression<Func<IUser, object>> orderBy, Direction orderDirection)
|
||||
{
|
||||
if (string.IsNullOrEmpty(orderBy)) return sql;
|
||||
if (orderBy == null) return sql;
|
||||
|
||||
var expressionMember = ExpressionHelper.GetMemberInfo(orderBy);
|
||||
var mapper = _mapperCollection[typeof(IUser)];
|
||||
var mappedField = mapper.Map(SqlContext.SqlSyntax, expressionMember.Name);
|
||||
|
||||
if (mappedField.IsNullOrWhiteSpace())
|
||||
throw new ArgumentException("Could not find a mapping for the column specified in the orderBy clause");
|
||||
|
||||
// beware! NPoco paging code parses the query to isolate the ORDER BY fragment,
|
||||
// using a regex that wants "([\w\.\[\]\(\)\s""`,]+)" - meaning that anything
|
||||
// else in orderBy is going to break NPoco / not be detected
|
||||
|
||||
// beware! NPoco paging code (in PagingHelper) collapses everything [foo].[bar]
|
||||
// to [bar] only, so we MUST use aliases, cannot use [table].[field]
|
||||
|
||||
// beware! pre-2012 SqlServer is using a convoluted syntax for paging, which
|
||||
// includes "SELECT ROW_NUMBER() OVER (ORDER BY ...) poco_rn FROM SELECT (...",
|
||||
// so anything added here MUST also be part of the inner SELECT statement, ie
|
||||
// the original statement, AND must be using the proper alias, as the inner SELECT
|
||||
// will hide the original table.field names entirely
|
||||
|
||||
var orderByField = sql.GetAliasedField(mappedField);
|
||||
|
||||
if (orderDirection == Direction.Ascending)
|
||||
sql.OrderBy(orderBy);
|
||||
sql.OrderBy(orderByField);
|
||||
else
|
||||
sql.OrderByDescending(orderBy);
|
||||
sql.OrderByDescending(orderByField);
|
||||
|
||||
return sql;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Data.Common;
|
||||
using System.Linq;
|
||||
using NPoco;
|
||||
@@ -122,6 +123,12 @@ namespace Umbraco.Core.Persistence.SqlSyntax
|
||||
SERVERPROPERTY('ResourceLastUpdateDateTime') ResourceLastUpdateDateTime,
|
||||
SERVERPROPERTY('ProductLevel') ProductLevel;";
|
||||
|
||||
string GetString(IDataReader reader, int ordinal, string defaultValue)
|
||||
=> reader.IsDBNull(ordinal) ? defaultValue : reader.GetString(ordinal);
|
||||
|
||||
int GetInt32(IDataReader reader, int ordinal, int defaultValue)
|
||||
=> reader.IsDBNull(ordinal) ? defaultValue : reader.GetInt32(ordinal);
|
||||
|
||||
connection.ConnectionString = connectionString;
|
||||
ServerVersionInfo version;
|
||||
using (connection)
|
||||
@@ -136,12 +143,12 @@ namespace Umbraco.Core.Persistence.SqlSyntax
|
||||
reader.Read();
|
||||
// InstanceName can be NULL for the default instance
|
||||
version = new ServerVersionInfo(
|
||||
reader.GetString(0),
|
||||
reader.IsDBNull(2) ? "(default)" : reader.GetString(2),
|
||||
reader.IsDBNull(3) ? "" : reader.GetString(3),
|
||||
(EngineEdition) reader.GetInt32(5),
|
||||
reader.GetString(7),
|
||||
reader.GetString(9));
|
||||
GetString(reader, 0, "Unknown"),
|
||||
GetString(reader, 2, "(default)"),
|
||||
GetString(reader, 3, string.Empty),
|
||||
(EngineEdition) GetInt32(reader, 5, 0),
|
||||
GetString(reader, 7, "DEFAULT"),
|
||||
GetString(reader, 9, "Unknown"));
|
||||
}
|
||||
connection.Close();
|
||||
}
|
||||
|
||||
@@ -290,7 +290,7 @@ namespace Umbraco.Core.PropertyEditors
|
||||
/// </summary>
|
||||
public IEnumerable<XElement> ConvertDbToXml(Property property, IDataTypeService dataTypeService, ILocalizationService localizationService, bool published)
|
||||
{
|
||||
published &= property.PropertyType.IsPublishing;
|
||||
published &= property.PropertyType.SupportsPublishing;
|
||||
|
||||
var nodeName = property.PropertyType.Alias.ToSafeAlias();
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ namespace Umbraco.Core.Scoping
|
||||
|
||||
// see note below
|
||||
if (scopeFileSystems == true)
|
||||
_fscope = fileSystems.Shadow(Guid.NewGuid());
|
||||
_fscope = fileSystems.Shadow();
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -105,7 +105,7 @@ namespace Umbraco.Core.Scoping
|
||||
// every scoped FS to trigger the creation of shadow FS "on demand", and that would be
|
||||
// pretty pointless since if scopeFileSystems is true, we *know* we want to shadow
|
||||
if (scopeFileSystems == true)
|
||||
_fscope = fileSystems.Shadow(Guid.NewGuid());
|
||||
_fscope = fileSystems.Shadow();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,16 +31,16 @@ namespace Umbraco.Core.Services
|
||||
/// <param name="contentService"></param>
|
||||
/// <param name="name"></param>
|
||||
/// <param name="parentId"></param>
|
||||
/// <param name="mediaTypeAlias"></param>
|
||||
/// <param name="contentTypeAlias"></param>
|
||||
/// <param name="userId"></param>
|
||||
/// <returns></returns>
|
||||
public static IContent CreateContent(this IContentService contentService, string name, Udi parentId, string mediaTypeAlias, int userId = Constants.Security.SuperUserId)
|
||||
public static IContent CreateContent(this IContentService contentService, string name, Udi parentId, string contentTypeAlias, int userId = Constants.Security.SuperUserId)
|
||||
{
|
||||
var guidUdi = parentId as GuidUdi;
|
||||
if (guidUdi == null)
|
||||
throw new InvalidOperationException("The UDI provided isn't of type " + typeof(GuidUdi) + " which is required by content");
|
||||
var parent = contentService.GetById(guidUdi.Guid);
|
||||
return contentService.Create(name, parent, mediaTypeAlias, userId);
|
||||
return contentService.Create(name, parent, contentTypeAlias, userId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -391,30 +391,30 @@ namespace Umbraco.Core.Services
|
||||
/// </remarks>
|
||||
IEnumerable<PublishResult> SaveAndPublishBranch(IContent content, bool force, string[] cultures, int userId = Constants.Security.SuperUserId);
|
||||
|
||||
/// <summary>
|
||||
/// Saves and publishes a document branch.
|
||||
/// </summary>
|
||||
/// <param name="content">The root document.</param>
|
||||
/// <param name="force">A value indicating whether to force-publish documents that are not already published.</param>
|
||||
/// <param name="shouldPublish">A function determining cultures to publish.</param>
|
||||
/// <param name="publishCultures">A function publishing cultures.</param>
|
||||
/// <param name="userId">The identifier of the user performing the operation.</param>
|
||||
/// <remarks>
|
||||
/// <para>The <paramref name="force"/> parameter determines which documents are published. When <c>false</c>,
|
||||
/// only those documents that are already published, are republished. When <c>true</c>, all documents are
|
||||
/// published. The root of the branch is always published, regardless of <paramref name="force"/>.</para>
|
||||
/// <para>The <paramref name="editing"/> parameter is a function which determines whether a document has
|
||||
/// changes to publish (else there is no need to publish it). If one wants to publish only a selection of
|
||||
/// cultures, one may want to check that only properties for these cultures have changed. Otherwise, other
|
||||
/// cultures may trigger an unwanted republish.</para>
|
||||
/// <para>The <paramref name="publishCultures"/> parameter is a function to execute to publish cultures, on
|
||||
/// each document. It can publish all, one, or a selection of cultures. It returns a boolean indicating
|
||||
/// whether the cultures could be published.</para>
|
||||
/// </remarks>
|
||||
IEnumerable<PublishResult> SaveAndPublishBranch(IContent content, bool force,
|
||||
Func<IContent, HashSet<string>> shouldPublish,
|
||||
Func<IContent, HashSet<string>, bool> publishCultures,
|
||||
int userId = Constants.Security.SuperUserId);
|
||||
///// <summary>
|
||||
///// Saves and publishes a document branch.
|
||||
///// </summary>
|
||||
///// <param name="content">The root document.</param>
|
||||
///// <param name="force">A value indicating whether to force-publish documents that are not already published.</param>
|
||||
///// <param name="shouldPublish">A function determining cultures to publish.</param>
|
||||
///// <param name="publishCultures">A function publishing cultures.</param>
|
||||
///// <param name="userId">The identifier of the user performing the operation.</param>
|
||||
///// <remarks>
|
||||
///// <para>The <paramref name="force"/> parameter determines which documents are published. When <c>false</c>,
|
||||
///// only those documents that are already published, are republished. When <c>true</c>, all documents are
|
||||
///// published. The root of the branch is always published, regardless of <paramref name="force"/>.</para>
|
||||
///// <para>The <paramref name="editing"/> parameter is a function which determines whether a document has
|
||||
///// changes to publish (else there is no need to publish it). If one wants to publish only a selection of
|
||||
///// cultures, one may want to check that only properties for these cultures have changed. Otherwise, other
|
||||
///// cultures may trigger an unwanted republish.</para>
|
||||
///// <para>The <paramref name="publishCultures"/> parameter is a function to execute to publish cultures, on
|
||||
///// each document. It can publish all, one, or a selection of cultures. It returns a boolean indicating
|
||||
///// whether the cultures could be published.</para>
|
||||
///// </remarks>
|
||||
//IEnumerable<PublishResult> SaveAndPublishBranch(IContent content, bool force,
|
||||
// Func<IContent, HashSet<string>> shouldPublish,
|
||||
// Func<IContent, HashSet<string>, bool> publishCultures,
|
||||
// int userId = Constants.Security.SuperUserId);
|
||||
|
||||
/// <summary>
|
||||
/// Unpublishes a document.
|
||||
|
||||
@@ -10,6 +10,7 @@ using Umbraco.Core.Models.Membership;
|
||||
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
|
||||
using Umbraco.Core.Persistence.Querying;
|
||||
using Umbraco.Core.Persistence.Repositories;
|
||||
using Umbraco.Core.PropertyEditors;
|
||||
using Umbraco.Core.Scoping;
|
||||
using Umbraco.Core.Services.Changes;
|
||||
|
||||
@@ -27,6 +28,8 @@ namespace Umbraco.Core.Services.Implement
|
||||
private readonly IDocumentBlueprintRepository _documentBlueprintRepository;
|
||||
private readonly ILanguageRepository _languageRepository;
|
||||
private IQuery<IContent> _queryNotTrashed;
|
||||
//TODO: The non-lazy object should be injected
|
||||
private readonly Lazy<PropertyValidationService> _propertyValidationService = new Lazy<PropertyValidationService>(() => new PropertyValidationService());
|
||||
|
||||
#region Constructors
|
||||
|
||||
@@ -193,7 +196,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
var content = new Content(name, parentId, contentType);
|
||||
using (var scope = ScopeProvider.CreateScope())
|
||||
{
|
||||
CreateContent(scope, content, parent, userId, false);
|
||||
CreateContent(scope, content, userId, false);
|
||||
scope.Complete();
|
||||
}
|
||||
|
||||
@@ -227,7 +230,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
throw new ArgumentException("No content type with that alias.", nameof(contentTypeAlias)); // causes rollback
|
||||
|
||||
var content = new Content(name, parent, contentType);
|
||||
CreateContent(scope, content, parent, userId, false);
|
||||
CreateContent(scope, content, userId, false);
|
||||
|
||||
scope.Complete();
|
||||
return content;
|
||||
@@ -261,7 +264,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
throw new ArgumentException("No content with that id.", nameof(parentId)); // causes rollback
|
||||
|
||||
var content = parentId > 0 ? new Content(name, parent, contentType) : new Content(name, parentId, contentType);
|
||||
CreateContent(scope, content, parent, userId, true);
|
||||
CreateContent(scope, content, userId, true);
|
||||
|
||||
scope.Complete();
|
||||
return content;
|
||||
@@ -293,14 +296,14 @@ namespace Umbraco.Core.Services.Implement
|
||||
throw new ArgumentException("No content type with that alias.", nameof(contentTypeAlias)); // causes rollback
|
||||
|
||||
var content = new Content(name, parent, contentType);
|
||||
CreateContent(scope, content, parent, userId, true);
|
||||
CreateContent(scope, content, userId, true);
|
||||
|
||||
scope.Complete();
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateContent(IScope scope, Content content, IContent parent, int userId, bool withIdentity)
|
||||
private void CreateContent(IScope scope, IContent content, int userId, bool withIdentity)
|
||||
{
|
||||
content.CreatorId = userId;
|
||||
content.WriterId = userId;
|
||||
@@ -311,12 +314,12 @@ namespace Umbraco.Core.Services.Implement
|
||||
|
||||
// if saving is cancelled, content remains without an identity
|
||||
var saveEventArgs = new ContentSavingEventArgs(content, evtMsgs);
|
||||
if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving"))
|
||||
if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving)))
|
||||
return;
|
||||
|
||||
_documentRepository.Save(content);
|
||||
|
||||
scope.Events.Dispatch(Saved, this, saveEventArgs.ToContentSavedEventArgs(), "Saved");
|
||||
scope.Events.Dispatch(Saved, this, saveEventArgs.ToContentSavedEventArgs(), nameof(Saved));
|
||||
scope.Events.Dispatch(TreeChanged, this, new TreeChange<IContent>(content, TreeChangeTypes.RefreshNode).ToEventArgs());
|
||||
}
|
||||
|
||||
@@ -758,7 +761,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
using (var scope = ScopeProvider.CreateScope())
|
||||
{
|
||||
var saveEventArgs = new ContentSavingEventArgs(content, evtMsgs);
|
||||
if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving"))
|
||||
if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving)))
|
||||
{
|
||||
scope.Complete();
|
||||
return OperationResult.Cancel(evtMsgs);
|
||||
@@ -783,7 +786,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
|
||||
if (raiseEvents)
|
||||
{
|
||||
scope.Events.Dispatch(Saved, this, saveEventArgs.ToContentSavedEventArgs(), "Saved");
|
||||
scope.Events.Dispatch(Saved, this, saveEventArgs.ToContentSavedEventArgs(), nameof(Saved));
|
||||
}
|
||||
var changeType = TreeChangeTypes.RefreshNode;
|
||||
scope.Events.Dispatch(TreeChanged, this, new TreeChange<IContent>(content, changeType).ToEventArgs());
|
||||
@@ -813,7 +816,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
using (var scope = ScopeProvider.CreateScope())
|
||||
{
|
||||
var saveEventArgs = new ContentSavingEventArgs(contentsA, evtMsgs);
|
||||
if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving"))
|
||||
if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving)))
|
||||
{
|
||||
scope.Complete();
|
||||
return OperationResult.Cancel(evtMsgs);
|
||||
@@ -833,7 +836,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
|
||||
if (raiseEvents)
|
||||
{
|
||||
scope.Events.Dispatch(Saved, this, saveEventArgs.ToContentSavedEventArgs(), "Saved");
|
||||
scope.Events.Dispatch(Saved, this, saveEventArgs.ToContentSavedEventArgs(), nameof(Saved));
|
||||
}
|
||||
scope.Events.Dispatch(TreeChanged, this, treeChanges.ToEventArgs());
|
||||
Audit(AuditType.Save, userId == -1 ? 0 : userId, Constants.System.Root, "Saved multiple content");
|
||||
@@ -866,26 +869,51 @@ namespace Umbraco.Core.Services.Implement
|
||||
throw new NotSupportedException($"Culture \"{culture}\" is not supported by invariant content types.");
|
||||
}
|
||||
|
||||
// if culture is specific, first publish the invariant values, then publish the culture itself.
|
||||
// if culture is '*', then publish them all (including variants)
|
||||
|
||||
// explicitly SaveAndPublish a specific culture also publishes invariant values
|
||||
if (!culture.IsNullOrWhiteSpace() && culture != "*")
|
||||
using (var scope = ScopeProvider.CreateScope())
|
||||
{
|
||||
// publish the invariant values
|
||||
var publishInvariant = content.PublishCulture(null);
|
||||
if (!publishInvariant)
|
||||
scope.WriteLock(Constants.Locks.ContentTree);
|
||||
|
||||
var saveEventArgs = new ContentSavingEventArgs(content, evtMsgs);
|
||||
if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving)))
|
||||
return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
|
||||
|
||||
Property[] invalidProperties;
|
||||
|
||||
// if culture is specific, first publish the invariant values, then publish the culture itself.
|
||||
// if culture is '*', then publish them all (including variants)
|
||||
|
||||
// explicitly SaveAndPublish a specific culture also publishes invariant values
|
||||
if (!culture.IsNullOrWhiteSpace() && culture != "*")
|
||||
{
|
||||
// publish the invariant values
|
||||
var publishInvariant = content.PublishCulture(null);
|
||||
if (!publishInvariant)
|
||||
return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, content);
|
||||
|
||||
//validate the property values
|
||||
if (!_propertyValidationService.Value.IsPropertyDataValid(content, out invalidProperties))
|
||||
return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, content)
|
||||
{
|
||||
InvalidProperties = invalidProperties
|
||||
};
|
||||
}
|
||||
|
||||
// publish the culture(s)
|
||||
var publishCulture = content.PublishCulture(culture);
|
||||
if (!publishCulture)
|
||||
return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, content);
|
||||
|
||||
//validate the property values
|
||||
if (!_propertyValidationService.Value.IsPropertyDataValid(content, out invalidProperties))
|
||||
return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, content)
|
||||
{
|
||||
InvalidProperties = invalidProperties
|
||||
};
|
||||
|
||||
var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, userId, raiseEvents);
|
||||
scope.Complete();
|
||||
return result;
|
||||
}
|
||||
|
||||
// publish the culture(s)
|
||||
var publishCulture = content.PublishCulture(culture);
|
||||
if (!publishCulture)
|
||||
return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, content);
|
||||
|
||||
// finally, "save publishing"
|
||||
// what happens next depends on whether the content can be published or not
|
||||
return CommitDocumentChanges(content, userId, raiseEvents);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -894,23 +922,40 @@ namespace Umbraco.Core.Services.Implement
|
||||
if (content == null) throw new ArgumentNullException(nameof(content));
|
||||
if (cultures == null) throw new ArgumentNullException(nameof(cultures));
|
||||
|
||||
var evtMsgs = EventMessagesFactory.Get();
|
||||
|
||||
var varies = content.ContentType.VariesByCulture();
|
||||
|
||||
if (cultures.Length == 0)
|
||||
using (var scope = ScopeProvider.CreateScope())
|
||||
{
|
||||
//no cultures specified and doesn't vary, so publish it, else nothing to publish
|
||||
return !varies
|
||||
? SaveAndPublish(content, userId: userId, raiseEvents: raiseEvents)
|
||||
: new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content);
|
||||
scope.WriteLock(Constants.Locks.ContentTree);
|
||||
|
||||
var evtMsgs = EventMessagesFactory.Get();
|
||||
var saveEventArgs = new ContentSavingEventArgs(content, evtMsgs);
|
||||
if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving)))
|
||||
return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
|
||||
|
||||
var varies = content.ContentType.VariesByCulture();
|
||||
|
||||
if (cultures.Length == 0)
|
||||
{
|
||||
//no cultures specified and doesn't vary, so publish it, else nothing to publish
|
||||
return !varies
|
||||
? SaveAndPublish(content, userId: userId, raiseEvents: raiseEvents)
|
||||
: new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content);
|
||||
}
|
||||
|
||||
|
||||
if (cultures.Select(content.PublishCulture).Any(isValid => !isValid))
|
||||
return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, content);
|
||||
|
||||
//validate the property values
|
||||
if (!_propertyValidationService.Value.IsPropertyDataValid(content, out var invalidProperties))
|
||||
return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, content)
|
||||
{
|
||||
InvalidProperties = invalidProperties
|
||||
};
|
||||
|
||||
var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, userId, raiseEvents);
|
||||
scope.Complete();
|
||||
return result;
|
||||
}
|
||||
|
||||
// TODO: currently, no way to know which one failed
|
||||
if (cultures.Select(content.PublishCulture).Any(isValid => !isValid))
|
||||
return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, content);
|
||||
|
||||
return CommitDocumentChanges(content, userId, raiseEvents);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -943,56 +988,87 @@ namespace Umbraco.Core.Services.Implement
|
||||
if (!content.Published)
|
||||
return new PublishResult(PublishResultType.SuccessUnpublishAlready, evtMsgs, content);
|
||||
|
||||
// all cultures = unpublish whole
|
||||
if (culture == "*" || (!content.ContentType.VariesByCulture() && culture == null))
|
||||
using (var scope = ScopeProvider.CreateScope())
|
||||
{
|
||||
content.PublishedState = PublishedState.Unpublishing;
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the culture we want to unpublish was already unpublished, nothing to do.
|
||||
// To check for that we need to lookup the persisted content item
|
||||
var persisted = content.HasIdentity ? GetById(content.Id) : null;
|
||||
scope.WriteLock(Constants.Locks.ContentTree);
|
||||
|
||||
if (persisted != null && !persisted.IsCulturePublished(culture))
|
||||
return new PublishResult(PublishResultType.SuccessUnpublishAlready, evtMsgs, content);
|
||||
var saveEventArgs = new ContentSavingEventArgs(content, evtMsgs);
|
||||
if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving)))
|
||||
return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
|
||||
|
||||
// unpublish the culture
|
||||
content.UnpublishCulture(culture);
|
||||
// all cultures = unpublish whole
|
||||
if (culture == "*" || (!content.ContentType.VariesByCulture() && culture == null))
|
||||
{
|
||||
content.PublishedState = PublishedState.Unpublishing;
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the culture we want to unpublish was already unpublished, nothing to do.
|
||||
// To check for that we need to lookup the persisted content item
|
||||
var persisted = content.HasIdentity ? GetById(content.Id) : null;
|
||||
|
||||
if (persisted != null && !persisted.IsCulturePublished(culture))
|
||||
return new PublishResult(PublishResultType.SuccessUnpublishAlready, evtMsgs, content);
|
||||
|
||||
// unpublish the culture
|
||||
content.UnpublishCulture(culture);
|
||||
}
|
||||
|
||||
var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, userId);
|
||||
scope.Complete();
|
||||
return result;
|
||||
}
|
||||
|
||||
// finally, "save publishing"
|
||||
return CommitDocumentChanges(content, userId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves a document and publishes/unpublishes any pending publishing changes made to the document.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This MUST NOT be called from within this service, this used to be a public API and must only be used outside of this service.
|
||||
/// Internally in this service, calls must be made to CommitDocumentChangesInternal
|
||||
/// </para>
|
||||
///
|
||||
/// <para>This is the underlying logic for both publishing and unpublishing any document</para>
|
||||
/// <para>Pending publishing/unpublishing changes on a document are made with calls to <see cref="IContent.PublishCulture"/> and
|
||||
/// <see cref="IContent.UnpublishCulture"/>.</para>
|
||||
/// <para>Pending publishing/unpublishing changes on a document are made with calls to <see cref="ContentRepositoryExtensions.PublishCulture"/> and
|
||||
/// <see cref="ContentRepositoryExtensions.UnpublishCulture"/>.</para>
|
||||
/// <para>When publishing or unpublishing a single culture, or all cultures, use <see cref="SaveAndPublish"/>
|
||||
/// and <see cref="Unpublish"/>. But if the flexibility to both publish and unpublish in a single operation is required
|
||||
/// then this method needs to be used in combination with <see cref="IContent.PublishCulture"/> and <see cref="IContent.UnpublishCulture"/>
|
||||
/// then this method needs to be used in combination with <see cref="ContentRepositoryExtensions.PublishCulture"/> and <see cref="ContentRepositoryExtensions.UnpublishCulture"/>
|
||||
/// on the content itself - this prepares the content, but does not commit anything - and then, invoke
|
||||
/// <see cref="CommitDocumentChanges"/> to actually commit the changes to the database.</para>
|
||||
/// <para>The document is *always* saved, even when publishing fails.</para>
|
||||
/// </remarks>
|
||||
internal PublishResult CommitDocumentChanges(IContent content, int userId = Constants.Security.SuperUserId, bool raiseEvents = true)
|
||||
internal PublishResult CommitDocumentChanges(IContent content,
|
||||
int userId = Constants.Security.SuperUserId, bool raiseEvents = true)
|
||||
{
|
||||
using (var scope = ScopeProvider.CreateScope())
|
||||
{
|
||||
var evtMsgs = EventMessagesFactory.Get();
|
||||
|
||||
scope.WriteLock(Constants.Locks.ContentTree);
|
||||
var result = CommitDocumentChangesInternal(scope, content, userId, raiseEvents);
|
||||
|
||||
var saveEventArgs = new ContentSavingEventArgs(content, evtMsgs);
|
||||
if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving)))
|
||||
return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
|
||||
|
||||
var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, userId, raiseEvents);
|
||||
scope.Complete();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private PublishResult CommitDocumentChangesInternal(IScope scope, IContent content, int userId = Constants.Security.SuperUserId, bool raiseEvents = true, bool branchOne = false, bool branchRoot = false)
|
||||
private PublishResult CommitDocumentChangesInternal(IScope scope, IContent content,
|
||||
ContentSavingEventArgs saveEventArgs,
|
||||
int userId = Constants.Security.SuperUserId, bool raiseEvents = true, bool branchOne = false, bool branchRoot = false)
|
||||
{
|
||||
var evtMsgs = EventMessagesFactory.Get();
|
||||
if (scope == null) throw new ArgumentNullException(nameof(scope));
|
||||
if (content == null) throw new ArgumentNullException(nameof(content));
|
||||
if (saveEventArgs == null) throw new ArgumentNullException(nameof(saveEventArgs));
|
||||
|
||||
var evtMsgs = saveEventArgs.Messages;
|
||||
|
||||
PublishResult publishResult = null;
|
||||
PublishResult unpublishResult = null;
|
||||
|
||||
@@ -1018,11 +1094,6 @@ namespace Umbraco.Core.Services.Implement
|
||||
var changeType = isNew ? TreeChangeTypes.RefreshNode : TreeChangeTypes.RefreshBranch;
|
||||
var previouslyPublished = content.HasIdentity && content.Published;
|
||||
|
||||
// always save
|
||||
var saveEventArgs = new ContentSavingEventArgs(content, evtMsgs);
|
||||
if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving"))
|
||||
return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
|
||||
|
||||
if (publishing)
|
||||
{
|
||||
culturesUnpublishing = content.GetCulturesUnpublishing();
|
||||
@@ -1058,7 +1129,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
// but: (a) it means we don't reproduce the PublishState logic here and (b) setting the
|
||||
// PublishState to anything other than Publishing or Unpublishing - which is precisely
|
||||
// what we want to do here - throws
|
||||
content.Published = content.Published;
|
||||
content.Published = content.Published;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1107,7 +1178,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
// raise the Saved event, always
|
||||
if (raiseEvents)
|
||||
{
|
||||
scope.Events.Dispatch(Saved, this, saveEventArgs.ToContentSavedEventArgs(), "Saved");
|
||||
scope.Events.Dispatch(Saved, this, saveEventArgs.ToContentSavedEventArgs(), nameof(Saved));
|
||||
}
|
||||
|
||||
if (unpublishing) // we have tried to unpublish - won't happen in a branch
|
||||
@@ -1242,6 +1313,13 @@ namespace Umbraco.Core.Services.Implement
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
if (pendingCultures.Count == 0)
|
||||
break; //shouldn't happen but no point in continuing if there's nothing there
|
||||
|
||||
var saveEventArgs = new ContentSavingEventArgs(d, evtMsgs);
|
||||
if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving)))
|
||||
yield return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d);
|
||||
|
||||
var publishing = true;
|
||||
foreach (var culture in pendingCultures)
|
||||
{
|
||||
@@ -1250,7 +1328,14 @@ namespace Umbraco.Core.Services.Implement
|
||||
|
||||
if (d.Trashed) continue; // won't publish
|
||||
|
||||
publishing &= d.PublishCulture(culture); //set the culture to be published
|
||||
//publish the culture values and validate the property values, if validation fails, log the invalid properties so the develeper has an idea of what has failed
|
||||
Property[] invalidProperties = null;
|
||||
var tryPublish = d.PublishCulture(culture) && _propertyValidationService.Value.IsPropertyDataValid(d, out invalidProperties);
|
||||
if (invalidProperties != null && invalidProperties.Length > 0)
|
||||
Logger.Warn<ContentService>("Scheduled publishing will fail for document {DocumentId} and culture {Culture} because of invalid properties {InvalidProperties}",
|
||||
d.Id, culture, string.Join(",", invalidProperties.Select(x => x.Alias)));
|
||||
|
||||
publishing &= tryPublish; //set the culture to be published
|
||||
if (!publishing) break; // no point continuing
|
||||
}
|
||||
|
||||
@@ -1259,7 +1344,8 @@ namespace Umbraco.Core.Services.Implement
|
||||
else if (!publishing)
|
||||
result = new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, d);
|
||||
else
|
||||
result = CommitDocumentChanges(d, d.WriterId);
|
||||
result = CommitDocumentChangesInternal(scope, d, saveEventArgs, d.WriterId);
|
||||
|
||||
|
||||
if (result.Success == false)
|
||||
Logger.Error<ContentService>(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
|
||||
@@ -1293,6 +1379,13 @@ namespace Umbraco.Core.Services.Implement
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
if (pendingCultures.Count == 0)
|
||||
break; //shouldn't happen but no point in continuing if there's nothing there
|
||||
|
||||
var saveEventArgs = new ContentSavingEventArgs(d, evtMsgs);
|
||||
if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving)))
|
||||
yield return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d);
|
||||
|
||||
foreach (var c in pendingCultures)
|
||||
{
|
||||
//Clear this schedule for this culture
|
||||
@@ -1301,13 +1394,11 @@ namespace Umbraco.Core.Services.Implement
|
||||
d.UnpublishCulture(c);
|
||||
}
|
||||
|
||||
if (pendingCultures.Count > 0)
|
||||
{
|
||||
result = CommitDocumentChanges(d, d.WriterId);
|
||||
if (result.Success == false)
|
||||
Logger.Error<ContentService>(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
|
||||
yield return result;
|
||||
}
|
||||
result = CommitDocumentChangesInternal(scope, d, saveEventArgs, d.WriterId);
|
||||
if (result.Success == false)
|
||||
Logger.Error<ContentService>(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
|
||||
yield return result;
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1328,16 +1419,20 @@ namespace Umbraco.Core.Services.Implement
|
||||
}
|
||||
}
|
||||
|
||||
private bool SaveAndPublishBranch_PublishCultures(IContent c, HashSet<string> culturesToPublish)
|
||||
// utility 'PublishCultures' func used by SaveAndPublishBranch
|
||||
private bool SaveAndPublishBranch_PublishCultures(IContent content, HashSet<string> culturesToPublish)
|
||||
{
|
||||
//TODO: This does not support being able to return invalid property details to bubble up to the UI
|
||||
|
||||
// variant content type - publish specified cultures
|
||||
// invariant content type - publish only the invariant culture
|
||||
return c.ContentType.VariesByCulture()
|
||||
? culturesToPublish.All(c.PublishCulture)
|
||||
: c.PublishCulture();
|
||||
return content.ContentType.VariesByCulture()
|
||||
? culturesToPublish.All(culture => content.PublishCulture(culture) && _propertyValidationService.Value.IsPropertyDataValid(content, out _))
|
||||
: content.PublishCulture() && _propertyValidationService.Value.IsPropertyDataValid(content, out _);
|
||||
}
|
||||
|
||||
private HashSet<string> SaveAndPublishBranch_ShouldPublish3(ref HashSet<string> cultures, string c, bool published, bool edited, bool isRoot, bool force)
|
||||
// utility 'ShouldPublish' func used by SaveAndPublishBranch
|
||||
private HashSet<string> SaveAndPublishBranch_ShouldPublish(ref HashSet<string> cultures, string c, bool published, bool edited, bool isRoot, bool force)
|
||||
{
|
||||
// if published, republish
|
||||
if (published)
|
||||
@@ -1355,7 +1450,6 @@ namespace Umbraco.Core.Services.Implement
|
||||
return cultures;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<PublishResult> SaveAndPublishBranch(IContent content, bool force, string culture = "*", int userId = Constants.Security.SuperUserId)
|
||||
{
|
||||
@@ -1374,10 +1468,10 @@ namespace Umbraco.Core.Services.Implement
|
||||
HashSet<string> culturesToPublish = null;
|
||||
|
||||
if (!c.ContentType.VariesByCulture()) // invariant content type
|
||||
return SaveAndPublishBranch_ShouldPublish3(ref culturesToPublish, "*", c.Published, c.Edited, isRoot, force);
|
||||
return SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, "*", c.Published, c.Edited, isRoot, force);
|
||||
|
||||
if (culture != "*") // variant content type, specific culture
|
||||
return SaveAndPublishBranch_ShouldPublish3(ref culturesToPublish, culture, c.IsCulturePublished(culture), c.IsCultureEdited(culture), isRoot, force);
|
||||
return SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, culture, c.IsCulturePublished(culture), c.IsCultureEdited(culture), isRoot, force);
|
||||
|
||||
// variant content type, all cultures
|
||||
if (c.Published)
|
||||
@@ -1385,7 +1479,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
// then some (and maybe all) cultures will be 'already published' (unless forcing),
|
||||
// others will have to 'republish this culture'
|
||||
foreach (var x in c.AvailableCultures)
|
||||
SaveAndPublishBranch_ShouldPublish3(ref culturesToPublish, x, c.IsCulturePublished(x), c.IsCultureEdited(x), isRoot, force);
|
||||
SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, x, c.IsCulturePublished(x), c.IsCultureEdited(x), isRoot, force);
|
||||
return culturesToPublish;
|
||||
}
|
||||
|
||||
@@ -1414,7 +1508,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
HashSet<string> culturesToPublish = null;
|
||||
|
||||
if (!c.ContentType.VariesByCulture()) // invariant content type
|
||||
return SaveAndPublishBranch_ShouldPublish3(ref culturesToPublish, "*", c.Published, c.Edited, isRoot, force);
|
||||
return SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, "*", c.Published, c.Edited, isRoot, force);
|
||||
|
||||
// variant content type, specific cultures
|
||||
if (c.Published)
|
||||
@@ -1422,7 +1516,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
// then some (and maybe all) cultures will be 'already published' (unless forcing),
|
||||
// others will have to 'republish this culture'
|
||||
foreach (var x in cultures)
|
||||
SaveAndPublishBranch_ShouldPublish3(ref culturesToPublish, x, c.IsCulturePublished(x), c.IsCultureEdited(x), isRoot, force);
|
||||
SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, x, c.IsCulturePublished(x), c.IsCultureEdited(x), isRoot, force);
|
||||
return culturesToPublish;
|
||||
}
|
||||
|
||||
@@ -1435,8 +1529,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
return SaveAndPublishBranch(content, force, ShouldPublish, SaveAndPublishBranch_PublishCultures, userId);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<PublishResult> SaveAndPublishBranch(IContent document, bool force,
|
||||
internal IEnumerable<PublishResult> SaveAndPublishBranch(IContent document, bool force,
|
||||
Func<IContent, HashSet<string>> shouldPublish,
|
||||
Func<IContent, HashSet<string>, bool> publishCultures,
|
||||
int userId = Constants.Security.SuperUserId)
|
||||
@@ -1460,7 +1553,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
throw new InvalidOperationException("Cannot mix PublishCulture and SaveAndPublishBranch.");
|
||||
|
||||
// deal with the branch root - if it fails, abort
|
||||
var result = SaveAndPublishBranchOne(scope, document, shouldPublish, publishCultures, true, publishedDocuments, evtMsgs, userId);
|
||||
var result = SaveAndPublishBranchItem(scope, document, shouldPublish, publishCultures, true, publishedDocuments, evtMsgs, userId);
|
||||
if (result != null)
|
||||
{
|
||||
results.Add(result);
|
||||
@@ -1491,7 +1584,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
}
|
||||
|
||||
// no need to check path here, parent has to be published here
|
||||
result = SaveAndPublishBranchOne(scope, d, shouldPublish, publishCultures, false, publishedDocuments, evtMsgs, userId);
|
||||
result = SaveAndPublishBranchItem(scope, d, shouldPublish, publishCultures, false, publishedDocuments, evtMsgs, userId);
|
||||
if (result != null)
|
||||
{
|
||||
results.Add(result);
|
||||
@@ -1521,7 +1614,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
// shouldPublish: a function determining whether the document has changes that need to be published
|
||||
// note - 'force' is handled by 'editing'
|
||||
// publishValues: a function publishing values (using the appropriate PublishCulture calls)
|
||||
private PublishResult SaveAndPublishBranchOne(IScope scope, IContent document,
|
||||
private PublishResult SaveAndPublishBranchItem(IScope scope, IContent document,
|
||||
Func<IContent, HashSet<string>> shouldPublish,
|
||||
Func<IContent, HashSet<string>, bool> publishCultures,
|
||||
bool isRoot,
|
||||
@@ -1534,11 +1627,18 @@ namespace Umbraco.Core.Services.Implement
|
||||
if (culturesToPublish.Count == 0) // empty = already published
|
||||
return new PublishResult(PublishResultType.SuccessPublishAlready, evtMsgs, document);
|
||||
|
||||
var saveEventArgs = new ContentSavingEventArgs(document, evtMsgs);
|
||||
if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving)))
|
||||
return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, document);
|
||||
|
||||
// publish & check if values are valid
|
||||
if (!publishCultures(document, culturesToPublish))
|
||||
{
|
||||
//TODO: Based on this callback behavior there is no way to know which properties may have been invalid if this failed, see other results of FailedPublishContentInvalid
|
||||
return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, document);
|
||||
}
|
||||
|
||||
var result = CommitDocumentChangesInternal(scope, document, userId, branchOne: true, branchRoot: isRoot);
|
||||
var result = CommitDocumentChangesInternal(scope, document, saveEventArgs, userId, branchOne: true, branchRoot: isRoot);
|
||||
if (result.Success)
|
||||
publishedDocuments.Add(document);
|
||||
return result;
|
||||
@@ -2513,7 +2613,6 @@ namespace Umbraco.Core.Services.Implement
|
||||
return new PublishResult(PublishResultType.SuccessPublishCulture, evtMsgs, content);
|
||||
}
|
||||
|
||||
|
||||
Logger.Info<ContentService>("Document {ContentName} (id={ContentId}) has been published.", content.Name, content.Id);
|
||||
return new PublishResult(evtMsgs, content);
|
||||
}
|
||||
|
||||
@@ -1175,7 +1175,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
var identity = int.MaxValue;
|
||||
|
||||
var memType = new MemberType(-1);
|
||||
var propGroup = new PropertyGroup(MemberType.IsPublishingConst)
|
||||
var propGroup = new PropertyGroup(MemberType.SupportsPublishingConst)
|
||||
{
|
||||
Name = "Membership",
|
||||
Id = --identity
|
||||
|
||||
@@ -3,26 +3,14 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using System.Xml.Linq;
|
||||
using Semver;
|
||||
using Umbraco.Core.Collections;
|
||||
using Umbraco.Core.Events;
|
||||
using Umbraco.Core.Exceptions;
|
||||
using Umbraco.Core.IO;
|
||||
using Umbraco.Core.Logging;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Models.Entities;
|
||||
using Umbraco.Core.Models.Packaging;
|
||||
using Umbraco.Core.Packaging;
|
||||
using Umbraco.Core.Persistence.Querying;
|
||||
using Umbraco.Core.Persistence.Repositories;
|
||||
using Umbraco.Core.PropertyEditors;
|
||||
using Umbraco.Core.Scoping;
|
||||
using Umbraco.Core.Strings;
|
||||
using Content = Umbraco.Core.Models.Content;
|
||||
|
||||
namespace Umbraco.Core.Services.Implement
|
||||
{
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Umbraco.Core.Collections;
|
||||
using Umbraco.Core.Composing;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.PropertyEditors;
|
||||
|
||||
namespace Umbraco.Core.Services
|
||||
{
|
||||
//TODO: We should make this an interface and inject it into the ContentService
|
||||
internal class PropertyValidationService
|
||||
{
|
||||
private readonly PropertyEditorCollection _propertyEditors;
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
|
||||
public PropertyValidationService(PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService)
|
||||
{
|
||||
_propertyEditors = propertyEditors;
|
||||
_dataTypeService = dataTypeService;
|
||||
}
|
||||
|
||||
//TODO: Remove this method in favor of the overload specifying all dependencies
|
||||
public PropertyValidationService()
|
||||
: this(Current.PropertyEditors, Current.Services.DataTypeService)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the content item's properties pass validation rules
|
||||
/// </summary>
|
||||
/// <para>If the content type is variant, then culture can be either '*' or an actual culture, but neither 'null' nor
|
||||
/// 'empty'. If the content type is invariant, then culture can be either '*' or null or empty.</para>
|
||||
public bool IsPropertyDataValid(IContentBase content, out Property[] invalidProperties, string culture = "*")
|
||||
{
|
||||
// select invalid properties
|
||||
invalidProperties = content.Properties.Where(x =>
|
||||
{
|
||||
// if culture is null, we validate invariant properties only
|
||||
// if culture is '*' we validate both variant and invariant properties, automatically
|
||||
// if culture is specific eg 'en-US' we both too, but explicitly
|
||||
|
||||
var varies = x.PropertyType.VariesByCulture();
|
||||
|
||||
if (culture == null)
|
||||
return !(varies || IsPropertyValid(x, null)); // validate invariant property, invariant culture
|
||||
|
||||
if (culture == "*")
|
||||
return !IsPropertyValid(x, culture); // validate property, all cultures
|
||||
|
||||
return varies
|
||||
? !IsPropertyValid(x, culture) // validate variant property, explicit culture
|
||||
: !IsPropertyValid(x, null); // validate invariant property, explicit culture
|
||||
})
|
||||
.ToArray();
|
||||
|
||||
return invalidProperties.Length == 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the property has valid values.
|
||||
/// </summary>
|
||||
public bool IsPropertyValid(Property property, string culture = "*", string segment = "*")
|
||||
{
|
||||
//NOTE - the pvalue and vvalues logic in here is borrowed directly from the Property.Values setter so if you are wondering what that's all about, look there.
|
||||
// The underlying Property._pvalue and Property._vvalues are not exposed but we can re-create these values ourselves which is what it's doing.
|
||||
|
||||
culture = culture.NullOrWhiteSpaceAsNull();
|
||||
segment = segment.NullOrWhiteSpaceAsNull();
|
||||
|
||||
Property.PropertyValue pvalue = null;
|
||||
|
||||
// if validating invariant/neutral, and it is supported, validate
|
||||
// (including ensuring that the value exists, if mandatory)
|
||||
if ((culture == null || culture == "*") && (segment == null || segment == "*") && property.PropertyType.SupportsVariation(null, null))
|
||||
{
|
||||
// validate pvalue (which is the invariant value)
|
||||
pvalue = property.Values.FirstOrDefault(x => x.Culture == null && x.Segment == null);
|
||||
if (!IsValidPropertyValue(property, pvalue?.EditedValue))
|
||||
return false;
|
||||
}
|
||||
|
||||
// if validating only invariant/neutral, we are good
|
||||
if (culture == null && segment == null)
|
||||
return true;
|
||||
|
||||
// if nothing else to validate, we are good
|
||||
if ((culture == null || culture == "*") && (segment == null || segment == "*") && !property.PropertyType.VariesByCulture())
|
||||
return true;
|
||||
|
||||
// for anything else, validate the existing values (including mandatory),
|
||||
// but we cannot validate mandatory globally (we don't know the possible cultures and segments)
|
||||
|
||||
// validate vvalues (which are the variant values)
|
||||
|
||||
// if we don't have vvalues (property.Values is empty or only contains pvalue), validate null
|
||||
if (property.Values.Count == (pvalue == null ? 0 : 1))
|
||||
return culture == "*" || IsValidPropertyValue(property, null);
|
||||
|
||||
// else validate vvalues (but don't revalidate pvalue)
|
||||
var pvalues = property.Values.Where(x =>
|
||||
x != pvalue && // don't revalidate pvalue
|
||||
property.PropertyType.SupportsVariation(x.Culture, x.Segment, true) && // the value variation is ok
|
||||
(culture == "*" || x.Culture.InvariantEquals(culture)) && // the culture matches
|
||||
(segment == "*" || x.Segment.InvariantEquals(segment))) // the segment matches
|
||||
.ToList();
|
||||
|
||||
return pvalues.Count == 0 || pvalues.All(x => IsValidPropertyValue(property, x.EditedValue));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Boolean indicating whether the passed in value is valid
|
||||
/// </summary>
|
||||
/// <param name="property"></param>
|
||||
/// <param name="value"></param>
|
||||
/// <returns>True is property value is valid, otherwise false</returns>
|
||||
private bool IsValidPropertyValue(Property property, object value)
|
||||
{
|
||||
return IsPropertyValueValid(property.PropertyType, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a value is valid for this property type.
|
||||
/// </summary>
|
||||
private bool IsPropertyValueValid(PropertyType propertyType, object value)
|
||||
{
|
||||
var editor = _propertyEditors[propertyType.PropertyEditorAlias];
|
||||
var configuration = _dataTypeService.GetDataType(propertyType.DataTypeId).Configuration;
|
||||
var valueEditor = editor.GetValueEditor(configuration);
|
||||
return !valueEditor.Validate(value, propertyType.Mandatory, propertyType.ValidationRegExp).Any();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -593,13 +593,14 @@ namespace Umbraco.Core
|
||||
///<returns></returns>
|
||||
public static string ToUrlBase64(this string input)
|
||||
{
|
||||
if (input == null) throw new ArgumentNullException("input");
|
||||
if (input == null) throw new ArgumentNullException(nameof(input));
|
||||
|
||||
if (String.IsNullOrEmpty(input)) return String.Empty;
|
||||
if (string.IsNullOrEmpty(input))
|
||||
return string.Empty;
|
||||
|
||||
//return Convert.ToBase64String(bytes).Replace(".", "-").Replace("/", "_").Replace("=", ",");
|
||||
var bytes = Encoding.UTF8.GetBytes(input);
|
||||
return UrlTokenEncode(bytes);
|
||||
//return Convert.ToBase64String(bytes).Replace(".", "-").Replace("/", "_").Replace("=", ",");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -609,14 +610,14 @@ namespace Umbraco.Core
|
||||
/// <returns></returns>
|
||||
public static string FromUrlBase64(this string input)
|
||||
{
|
||||
if (input == null) throw new ArgumentNullException("input");
|
||||
if (input == null) throw new ArgumentNullException(nameof(input));
|
||||
|
||||
//if (input.IsInvalidBase64()) return null;
|
||||
|
||||
try
|
||||
{
|
||||
//var decodedBytes = Convert.FromBase64String(input.Replace("-", ".").Replace("_", "/").Replace(",", "="));
|
||||
byte[] decodedBytes = UrlTokenDecode(input);
|
||||
var decodedBytes = UrlTokenDecode(input);
|
||||
return decodedBytes != null ? Encoding.UTF8.GetString(decodedBytes) : null;
|
||||
}
|
||||
catch (FormatException)
|
||||
@@ -795,42 +796,40 @@ namespace Umbraco.Core
|
||||
internal static byte[] UrlTokenDecode(string input)
|
||||
{
|
||||
if (input == null)
|
||||
throw new ArgumentNullException(nameof(input));
|
||||
|
||||
if (input.Length == 0)
|
||||
return Array.Empty<byte>();
|
||||
|
||||
// calc array size - must be groups of 4
|
||||
var arrayLength = input.Length;
|
||||
var remain = arrayLength % 4;
|
||||
if (remain != 0) arrayLength += 4 - remain;
|
||||
|
||||
var inArray = new char[arrayLength];
|
||||
for (var i = 0; i < input.Length; i++)
|
||||
{
|
||||
throw new ArgumentNullException("input");
|
||||
}
|
||||
int length = input.Length;
|
||||
if (length < 1)
|
||||
{
|
||||
return new byte[0];
|
||||
}
|
||||
int num2 = input[length - 1] - '0';
|
||||
if ((num2 < 0) || (num2 > 10))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
char[] inArray = new char[(length - 1) + num2];
|
||||
for (int i = 0; i < (length - 1); i++)
|
||||
{
|
||||
char ch = input[i];
|
||||
var ch = input[i];
|
||||
switch (ch)
|
||||
{
|
||||
case '-':
|
||||
case '-': // restore '-' as '+'
|
||||
inArray[i] = '+';
|
||||
break;
|
||||
|
||||
case '_':
|
||||
case '_': // restore '_' as '/'
|
||||
inArray[i] = '/';
|
||||
break;
|
||||
|
||||
default:
|
||||
default: // keep char unchanged
|
||||
inArray[i] = ch;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (int j = length - 1; j < inArray.Length; j++)
|
||||
{
|
||||
|
||||
// pad with '='
|
||||
for (var j = input.Length; j < inArray.Length; j++)
|
||||
inArray[j] = '=';
|
||||
}
|
||||
|
||||
return Convert.FromBase64CharArray(inArray, 0, inArray.Length);
|
||||
}
|
||||
|
||||
@@ -842,54 +841,40 @@ namespace Umbraco.Core
|
||||
internal static string UrlTokenEncode(byte[] input)
|
||||
{
|
||||
if (input == null)
|
||||
throw new ArgumentNullException(nameof(input));
|
||||
|
||||
if (input.Length == 0)
|
||||
return string.Empty;
|
||||
|
||||
// base-64 digits are A-Z, a-z, 0-9, + and /
|
||||
// the = char is used for trailing padding
|
||||
|
||||
var str = Convert.ToBase64String(input);
|
||||
|
||||
var pos = str.IndexOf('=');
|
||||
if (pos < 0) pos = str.Length;
|
||||
|
||||
// replace chars that would cause problems in urls
|
||||
var chArray = new char[pos];
|
||||
for (var i = 0; i < pos; i++)
|
||||
{
|
||||
throw new ArgumentNullException("input");
|
||||
}
|
||||
if (input.Length < 1)
|
||||
{
|
||||
return String.Empty;
|
||||
}
|
||||
string str = null;
|
||||
int index = 0;
|
||||
char[] chArray = null;
|
||||
str = Convert.ToBase64String(input);
|
||||
if (str == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
index = str.Length;
|
||||
while (index > 0)
|
||||
{
|
||||
if (str[index - 1] != '=')
|
||||
{
|
||||
break;
|
||||
}
|
||||
index--;
|
||||
}
|
||||
chArray = new char[index + 1];
|
||||
chArray[index] = (char)((0x30 + str.Length) - index);
|
||||
for (int i = 0; i < index; i++)
|
||||
{
|
||||
char ch = str[i];
|
||||
var ch = str[i];
|
||||
switch (ch)
|
||||
{
|
||||
case '+':
|
||||
case '+': // replace '+' with '-'
|
||||
chArray[i] = '-';
|
||||
break;
|
||||
|
||||
case '/':
|
||||
case '/': // replace '/' with '_'
|
||||
chArray[i] = '_';
|
||||
break;
|
||||
|
||||
case '=':
|
||||
chArray[i] = ch;
|
||||
break;
|
||||
|
||||
default:
|
||||
default: // keep char unchanged
|
||||
chArray[i] = ch;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return new string(chArray);
|
||||
}
|
||||
|
||||
|
||||
@@ -161,7 +161,7 @@
|
||||
<Compile Include="Composing\ComponentComposer.cs" />
|
||||
<Compile Include="Composing\Composers.cs" />
|
||||
<Compile Include="Composing\Composition.cs" />
|
||||
<Compile Include="CompositionExtensions_Accessors.cs" />
|
||||
<Compile Include="CompositionExtensions.cs" />
|
||||
<Compile Include="Composing\DisableAttribute.cs" />
|
||||
<Compile Include="Composing\DisableComposerAttribute.cs" />
|
||||
<Compile Include="Composing\EnableAttribute.cs" />
|
||||
@@ -187,6 +187,8 @@
|
||||
<Compile Include="Composing\CompositionExtensions\Repositories.cs" />
|
||||
<Compile Include="Composing\CompositionExtensions\Services.cs" />
|
||||
<Compile Include="CompositionExtensions_Essentials.cs" />
|
||||
<Compile Include="CompositionExtensions_FileSystems.cs" />
|
||||
<Compile Include="CompositionExtensions_Uniques.cs" />
|
||||
<Compile Include="FactoryExtensions.cs" />
|
||||
<Compile Include="Composing\RegisterFactory.cs" />
|
||||
<Compile Include="Composing\Current.cs" />
|
||||
@@ -205,9 +207,12 @@
|
||||
<Compile Include="Composing\TypeFinder.cs" />
|
||||
<Compile Include="Composing\TypeHelper.cs" />
|
||||
<Compile Include="Composing\TypeLoader.cs" />
|
||||
<Compile Include="IO\MediaPathSchemes\UniqueMediaPathScheme.cs" />
|
||||
<Compile Include="Migrations\Upgrade\V_8_0_0\MergeDateAndDateTimePropertyEditor.cs" />
|
||||
<Compile Include="Models\PublishedContent\ILivePublishedModelFactory.cs" />
|
||||
<Compile Include="PropertyEditors\DateTimeConfiguration.cs" />
|
||||
<Compile Include="Migrations\Upgrade\V_8_0_0\RenameLabelAndRichTextPropertyEditorAliases.cs" />
|
||||
<Compile Include="Services\PropertyValidationService.cs" />
|
||||
<Compile Include="TypeLoaderExtensions.cs" />
|
||||
<Compile Include="Composing\WeightAttribute.cs" />
|
||||
<Compile Include="Composing\WeightedCollectionBuilderBase.cs" />
|
||||
|
||||
@@ -15,6 +15,14 @@ namespace Umbraco.Tests.CoreThings
|
||||
Assert.AreEqual(GuidUtils.Combine(a, b).ToByteArray(), Combine(a, b));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GuidThingTest()
|
||||
{
|
||||
var guid = new Guid("f918382f-2bba-453f-a3e2-1f594016ed3b");
|
||||
Assert.AreEqual("f22br4n0fm5fli5c", GuidUtils.ToBase32String(guid, 16));
|
||||
Assert.AreEqual("f22br4n0f", GuidUtils.ToBase32String(guid, 9));
|
||||
}
|
||||
|
||||
// Reference implementation taken from original code.
|
||||
private static byte[] Combine(Guid guid1, Guid guid2)
|
||||
{
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace Umbraco.Tests.IO
|
||||
composition.Register(_ => Mock.Of<ILogger>());
|
||||
composition.Register(_ => Mock.Of<IDataTypeService>());
|
||||
composition.Register(_ => Mock.Of<IContentSection>());
|
||||
composition.RegisterUnique<IMediaPathScheme, OriginalMediaPathScheme>();
|
||||
composition.RegisterUnique<IMediaPathScheme, UniqueMediaPathScheme>();
|
||||
|
||||
composition.Configs.Add(SettingsForTests.GetDefaultGlobalSettings);
|
||||
composition.Configs.Add(SettingsForTests.GetDefaultUmbracoSettings);
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using NUnit.Framework;
|
||||
@@ -44,6 +42,34 @@ namespace Umbraco.Tests.IO
|
||||
return "/Media/" + path;
|
||||
}
|
||||
|
||||
private string Repeat(string pattern, int count)
|
||||
{
|
||||
var text = new StringBuilder();
|
||||
for (var i = 0; i < count; i++)
|
||||
text.Append(pattern);
|
||||
return text.ToString();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SaveFileTest()
|
||||
{
|
||||
var basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "FileSysTests");
|
||||
|
||||
using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo")))
|
||||
_fileSystem.AddFile("sub/f3.txt", ms);
|
||||
|
||||
Assert.IsTrue(File.Exists(Path.Combine(basePath, "sub/f3.txt")));
|
||||
|
||||
var path = Repeat("bah/bah/", 50);
|
||||
Assert.Less(260, path.Length);
|
||||
|
||||
Assert.Throws<PathTooLongException>(() =>
|
||||
{
|
||||
using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo")))
|
||||
_fileSystem.AddFile(path + "f3.txt", ms);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetFullPathTest()
|
||||
{
|
||||
@@ -60,14 +86,31 @@ namespace Umbraco.Tests.IO
|
||||
// - does properly normalize separators
|
||||
// - does throw on invalid paths
|
||||
|
||||
// works
|
||||
var path = _fileSystem.GetFullPath("foo.tmp");
|
||||
Assert.AreEqual(Path.Combine(basePath, @"foo.tmp"), path);
|
||||
|
||||
// a very long relative path, which ends up being a short path, works
|
||||
path = Repeat("bah/../", 50);
|
||||
Assert.Less(260, path.Length);
|
||||
path = _fileSystem.GetFullPath(path + "foo.tmp");
|
||||
Assert.AreEqual(Path.Combine(basePath, @"foo.tmp"), path);
|
||||
|
||||
// works too
|
||||
path = _fileSystem.GetFullPath("foo/bar.tmp");
|
||||
Assert.AreEqual(Path.Combine(basePath, @"foo\bar.tmp"), path);
|
||||
|
||||
// that path is invalid as it would be outside the root directory
|
||||
Assert.Throws<FileSecurityException>(() => _fileSystem.GetFullPath("../../foo.tmp"));
|
||||
|
||||
// a very long path, which ends up being very long, works
|
||||
path = Repeat("bah/bah/", 50);
|
||||
Assert.Less(260, path.Length);
|
||||
Assert.Throws<PathTooLongException>(() =>
|
||||
{
|
||||
path = _fileSystem.GetFullPath(path + "foo.tmp");
|
||||
Assert.Less(260, path.Length); // gets a >260 path and it's fine (but Windows will not like it)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -405,10 +405,10 @@ namespace Umbraco.Tests.IO
|
||||
sw.AddFile("sub/f1.txt", ms);
|
||||
Assert.IsTrue(phy.FileExists("sub/f1.txt"));
|
||||
|
||||
Guid id;
|
||||
string id;
|
||||
|
||||
// explicit shadow without scope does not work
|
||||
sw.Shadow(id = Guid.NewGuid());
|
||||
sw.Shadow(id = ShadowWrapper.CreateShadowId());
|
||||
Assert.IsTrue(Directory.Exists(shadowfs + "/" + id));
|
||||
using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo")))
|
||||
sw.AddFile("sub/f2.txt", ms);
|
||||
@@ -419,7 +419,7 @@ namespace Umbraco.Tests.IO
|
||||
|
||||
// shadow with scope but no complete does not complete
|
||||
scopedFileSystems = true; // pretend we have a scope
|
||||
var scope = new ShadowFileSystems(fileSystems, id = Guid.NewGuid());
|
||||
var scope = new ShadowFileSystems(fileSystems, id = ShadowWrapper.CreateShadowId());
|
||||
Assert.IsTrue(Directory.Exists(shadowfs + "/" + id));
|
||||
using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo")))
|
||||
sw.AddFile("sub/f3.txt", ms);
|
||||
@@ -428,10 +428,11 @@ namespace Umbraco.Tests.IO
|
||||
Assert.AreEqual(1, dirs.Length);
|
||||
Assert.AreEqual((shadowfs + "/" + id).Replace('\\', '/'), dirs[0].Replace('\\', '/'));
|
||||
dirs = Directory.GetDirectories(dirs[0]);
|
||||
var typedDir = dirs.FirstOrDefault(x => x.Replace('\\', '/').EndsWith("/typed"));
|
||||
var typedDir = dirs.FirstOrDefault(x => x.Replace('\\', '/').EndsWith("/x"));
|
||||
Assert.IsNotNull(typedDir);
|
||||
dirs = Directory.GetDirectories(typedDir);
|
||||
var scopedDir = dirs.FirstOrDefault(x => x.Replace('\\', '/').EndsWith("/Umbraco.Tests.IO.ShadowFileSystemTests+FS")); // this is where files go
|
||||
var suid = fileSystems.Paths[typeof(FS)];
|
||||
var scopedDir = dirs.FirstOrDefault(x => x.Replace('\\', '/').EndsWith("/" + suid)); // this is where files go
|
||||
Assert.IsNotNull(scopedDir);
|
||||
scope.Dispose();
|
||||
scopedFileSystems = false;
|
||||
@@ -440,7 +441,7 @@ namespace Umbraco.Tests.IO
|
||||
|
||||
// shadow with scope and complete does complete
|
||||
scopedFileSystems = true; // pretend we have a scope
|
||||
scope = new ShadowFileSystems(fileSystems, id = Guid.NewGuid());
|
||||
scope = new ShadowFileSystems(fileSystems, id = ShadowWrapper.CreateShadowId());
|
||||
Assert.IsTrue(Directory.Exists(shadowfs + "/" + id));
|
||||
using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo")))
|
||||
sw.AddFile("sub/f4.txt", ms);
|
||||
@@ -456,7 +457,7 @@ namespace Umbraco.Tests.IO
|
||||
// test scope for "another thread"
|
||||
|
||||
scopedFileSystems = true; // pretend we have a scope
|
||||
scope = new ShadowFileSystems(fileSystems, id = Guid.NewGuid());
|
||||
scope = new ShadowFileSystems(fileSystems, id = ShadowWrapper.CreateShadowId());
|
||||
Assert.IsTrue(Directory.Exists(shadowfs + "/" + id));
|
||||
using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo")))
|
||||
sw.AddFile("sub/f5.txt", ms);
|
||||
@@ -498,10 +499,10 @@ namespace Umbraco.Tests.IO
|
||||
sw.AddFile("sub/f1.txt", ms);
|
||||
Assert.IsTrue(phy.FileExists("sub/f1.txt"));
|
||||
|
||||
Guid id;
|
||||
string id;
|
||||
|
||||
scopedFileSystems = true; // pretend we have a scope
|
||||
var scope = new ShadowFileSystems(fileSystems, id = Guid.NewGuid());
|
||||
var scope = new ShadowFileSystems(fileSystems, id = ShadowWrapper.CreateShadowId());
|
||||
Assert.IsTrue(Directory.Exists(shadowfs + "/" + id));
|
||||
using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo")))
|
||||
sw.AddFile("sub/f2.txt", ms);
|
||||
@@ -551,10 +552,10 @@ namespace Umbraco.Tests.IO
|
||||
sw.AddFile("sub/f1.txt", ms);
|
||||
Assert.IsTrue(phy.FileExists("sub/f1.txt"));
|
||||
|
||||
Guid id;
|
||||
string id;
|
||||
|
||||
scopedFileSystems = true; // pretend we have a scope
|
||||
var scope = new ShadowFileSystems(fileSystems, id = Guid.NewGuid());
|
||||
var scope = new ShadowFileSystems(fileSystems, id = ShadowWrapper.CreateShadowId());
|
||||
Assert.IsTrue(Directory.Exists(shadowfs + "/" + id));
|
||||
using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo")))
|
||||
sw.AddFile("sub/f2.txt", ms);
|
||||
|
||||
@@ -135,7 +135,7 @@ namespace Umbraco.Tests.Models
|
||||
Assert.Throws<NotSupportedException>(() => prop.PublishValues());
|
||||
|
||||
// change
|
||||
propertyType.IsPublishing = true;
|
||||
propertyType.SupportsPublishing = true;
|
||||
|
||||
// can get value
|
||||
// and now published value is null
|
||||
@@ -364,6 +364,7 @@ namespace Umbraco.Tests.Models
|
||||
[Test]
|
||||
public void ContentPublishValuesWithMixedPropertyTypeVariations()
|
||||
{
|
||||
var propertyValidationService = new PropertyValidationService(Current.Factory.GetInstance<PropertyEditorCollection>(), Current.Factory.GetInstance<ServiceContext>().DataTypeService);
|
||||
const string langFr = "fr-FR";
|
||||
|
||||
// content type varies by Culture
|
||||
@@ -383,11 +384,15 @@ namespace Umbraco.Tests.Models
|
||||
|
||||
content.SetCultureName("hello", langFr);
|
||||
|
||||
Assert.IsFalse(content.PublishCulture(langFr)); // fails because prop1 is mandatory
|
||||
Assert.IsTrue(content.PublishCulture(langFr)); // succeeds because names are ok (not validating properties here)
|
||||
Assert.IsFalse(propertyValidationService.IsPropertyDataValid(content, out _, langFr));// fails because prop1 is mandatory
|
||||
|
||||
content.SetValue("prop1", "a", langFr);
|
||||
Assert.IsFalse(content.PublishCulture(langFr)); // fails because prop2 is mandatory and invariant
|
||||
Assert.IsTrue(content.PublishCulture(langFr)); // succeeds because names are ok (not validating properties here)
|
||||
Assert.IsFalse(propertyValidationService.IsPropertyDataValid(content, out _, langFr));// fails because prop2 is mandatory and invariant
|
||||
content.SetValue("prop2", "x");
|
||||
Assert.IsTrue(content.PublishCulture(langFr)); // now it's ok
|
||||
Assert.IsTrue(content.PublishCulture(langFr)); // still ok...
|
||||
Assert.IsTrue(propertyValidationService.IsPropertyDataValid(content, out _, langFr));// now it's ok
|
||||
|
||||
Assert.AreEqual("a", content.GetValue("prop1", langFr, published: true));
|
||||
Assert.AreEqual("x", content.GetValue("prop2", published: true));
|
||||
@@ -480,20 +485,21 @@ namespace Umbraco.Tests.Models
|
||||
[Test]
|
||||
public void ValidationTests()
|
||||
{
|
||||
var propertyType = new PropertyType("editor", ValueStorageType.Nvarchar) { Alias = "prop", IsPublishing = true };
|
||||
var propertyType = new PropertyType("editor", ValueStorageType.Nvarchar) { Alias = "prop", SupportsPublishing = true };
|
||||
var prop = new Property(propertyType);
|
||||
|
||||
prop.SetValue("a");
|
||||
Assert.AreEqual("a", prop.GetValue());
|
||||
Assert.IsNull(prop.GetValue(published: true));
|
||||
var propertyValidationService = new PropertyValidationService(Current.Factory.GetInstance<PropertyEditorCollection>(), Current.Factory.GetInstance<ServiceContext>().DataTypeService);
|
||||
|
||||
Assert.IsTrue(prop.IsValid());
|
||||
Assert.IsTrue(propertyValidationService.IsPropertyValid(prop));
|
||||
|
||||
propertyType.Mandatory = true;
|
||||
Assert.IsTrue(prop.IsValid());
|
||||
Assert.IsTrue(propertyValidationService.IsPropertyValid(prop));
|
||||
|
||||
prop.SetValue(null);
|
||||
Assert.IsFalse(prop.IsValid());
|
||||
Assert.IsFalse(propertyValidationService.IsPropertyValid(prop));
|
||||
|
||||
// can publish, even though invalid
|
||||
prop.PublishValues();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Events;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Persistence.Repositories.Implement;
|
||||
@@ -30,7 +31,7 @@ namespace Umbraco.Tests.Services
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SavingTest()
|
||||
public void Saving_Culture()
|
||||
{
|
||||
var languageService = ServiceContext.LocalizationService;
|
||||
|
||||
@@ -80,13 +81,68 @@ namespace Umbraco.Tests.Services
|
||||
|
||||
ContentService.Saving += OnSaving;
|
||||
ContentService.Saved += OnSaved;
|
||||
contentService.Save(document);
|
||||
ContentService.Saving -= OnSaving;
|
||||
ContentService.Saved -= OnSaved;
|
||||
try
|
||||
{
|
||||
contentService.Save(document);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ContentService.Saving -= OnSaving;
|
||||
ContentService.Saved -= OnSaved;
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PublishingTest()
|
||||
public void Saving_Set_Value()
|
||||
{
|
||||
var contentTypeService = ServiceContext.ContentTypeService;
|
||||
|
||||
var contentType = MockedContentTypes.CreateTextPageContentType();
|
||||
ServiceContext.FileService.SaveTemplate(contentType.DefaultTemplate);
|
||||
contentTypeService.Save(contentType);
|
||||
|
||||
var contentService = ServiceContext.ContentService;
|
||||
|
||||
IContent document = new Content("content", -1, contentType);
|
||||
|
||||
void OnSaving(IContentService sender, ContentSavingEventArgs e)
|
||||
{
|
||||
var saved = e.SavedEntities.First();
|
||||
|
||||
Assert.IsTrue(document.GetValue<string>("title").IsNullOrWhiteSpace());
|
||||
|
||||
saved.SetValue("title", "title");
|
||||
}
|
||||
|
||||
void OnSaved(IContentService sender, ContentSavedEventArgs e)
|
||||
{
|
||||
var saved = e.SavedEntities.First();
|
||||
|
||||
Assert.AreSame("title", document.GetValue<string>("title"));
|
||||
|
||||
//we're only dealing with invariant here
|
||||
var propValue = saved.Properties["title"].Values.First(x => x.Culture == null && x.Segment == null);
|
||||
|
||||
Assert.AreEqual("title", propValue.EditedValue);
|
||||
Assert.IsNull(propValue.PublishedValue);
|
||||
}
|
||||
|
||||
ContentService.Saving += OnSaving;
|
||||
ContentService.Saved += OnSaved;
|
||||
try
|
||||
{
|
||||
contentService.Save(document);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ContentService.Saving -= OnSaving;
|
||||
ContentService.Saved -= OnSaved;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Publishing_Culture()
|
||||
{
|
||||
var languageService = ServiceContext.LocalizationService;
|
||||
|
||||
@@ -136,9 +192,15 @@ namespace Umbraco.Tests.Services
|
||||
|
||||
ContentService.Publishing += OnPublishing;
|
||||
ContentService.Published += OnPublished;
|
||||
contentService.SaveAndPublish(document, "fr-FR");
|
||||
ContentService.Publishing -= OnPublishing;
|
||||
ContentService.Published -= OnPublished;
|
||||
try
|
||||
{
|
||||
contentService.SaveAndPublish(document, "fr-FR");
|
||||
}
|
||||
finally
|
||||
{
|
||||
ContentService.Publishing -= OnPublishing;
|
||||
ContentService.Published -= OnPublished;
|
||||
}
|
||||
|
||||
document = contentService.GetById(document.Id);
|
||||
|
||||
@@ -148,7 +210,105 @@ namespace Umbraco.Tests.Services
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void UnpublishingTest()
|
||||
public void Publishing_Set_Value()
|
||||
{
|
||||
var contentTypeService = ServiceContext.ContentTypeService;
|
||||
|
||||
var contentType = MockedContentTypes.CreateTextPageContentType();
|
||||
ServiceContext.FileService.SaveTemplate(contentType.DefaultTemplate);
|
||||
contentTypeService.Save(contentType);
|
||||
|
||||
var contentService = ServiceContext.ContentService;
|
||||
|
||||
IContent document = new Content("content", -1, contentType);
|
||||
|
||||
void OnSaving(IContentService sender, ContentSavingEventArgs e)
|
||||
{
|
||||
var saved = e.SavedEntities.First();
|
||||
|
||||
Assert.IsTrue(document.GetValue<string>("title").IsNullOrWhiteSpace());
|
||||
|
||||
saved.SetValue("title", "title");
|
||||
}
|
||||
|
||||
void OnSaved(IContentService sender, ContentSavedEventArgs e)
|
||||
{
|
||||
var saved = e.SavedEntities.First();
|
||||
|
||||
Assert.AreSame("title", document.GetValue<string>("title"));
|
||||
|
||||
//we're only dealing with invariant here
|
||||
var propValue = saved.Properties["title"].Values.First(x => x.Culture == null && x.Segment == null);
|
||||
|
||||
Assert.AreEqual("title", propValue.EditedValue);
|
||||
Assert.AreEqual("title", propValue.PublishedValue);
|
||||
}
|
||||
|
||||
//We are binding to Saving (not Publishing), because the Publishing event is really just used for cancelling, it should not be
|
||||
//used for setting values and it won't actually work! This is because the Publishing event is raised AFTER the values on the model
|
||||
//are published, but Saving is raised BEFORE.
|
||||
ContentService.Saving += OnSaving;
|
||||
ContentService.Saved += OnSaved;
|
||||
try
|
||||
{
|
||||
contentService.SaveAndPublish(document);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ContentService.Saving -= OnSaving;
|
||||
ContentService.Saved -= OnSaved;
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Publishing_Set_Mandatory_Value()
|
||||
{
|
||||
var contentTypeService = ServiceContext.ContentTypeService;
|
||||
|
||||
var contentType = MockedContentTypes.CreateTextPageContentType();
|
||||
var titleProperty = contentType.PropertyTypes.First(x => x.Alias == "title");
|
||||
titleProperty.Mandatory = true; // make this required!
|
||||
ServiceContext.FileService.SaveTemplate(contentType.DefaultTemplate);
|
||||
contentTypeService.Save(contentType);
|
||||
|
||||
var contentService = ServiceContext.ContentService;
|
||||
|
||||
IContent document = new Content("content", -1, contentType);
|
||||
|
||||
var result = contentService.SaveAndPublish(document);
|
||||
Assert.IsFalse(result.Success);
|
||||
Assert.AreEqual("title", result.InvalidProperties.First().Alias);
|
||||
|
||||
// when a service operation fails, the object is dirty and should not be re-used,
|
||||
// re-create it
|
||||
document = new Content("content", -1, contentType);
|
||||
|
||||
void OnSaving(IContentService sender, ContentSavingEventArgs e)
|
||||
{
|
||||
var saved = e.SavedEntities.First();
|
||||
|
||||
Assert.IsTrue(document.GetValue<string>("title").IsNullOrWhiteSpace());
|
||||
|
||||
saved.SetValue("title", "title");
|
||||
}
|
||||
|
||||
//We are binding to Saving (not Publishing), because the Publishing event is really just used for cancelling, it should not be
|
||||
//used for setting values and it won't actually work! This is because the Publishing event is raised AFTER the values on the model
|
||||
//are published, but Saving is raised BEFORE.
|
||||
ContentService.Saving += OnSaving;
|
||||
try
|
||||
{
|
||||
result = contentService.SaveAndPublish(document);
|
||||
Assert.IsTrue(result.Success); //will succeed now because we were able to specify the required value in the Saving event
|
||||
}
|
||||
finally
|
||||
{
|
||||
ContentService.Saving -= OnSaving;
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Unpublishing_Culture()
|
||||
{
|
||||
var languageService = ServiceContext.LocalizationService;
|
||||
|
||||
@@ -206,9 +366,15 @@ namespace Umbraco.Tests.Services
|
||||
|
||||
ContentService.Publishing += OnPublishing;
|
||||
ContentService.Published += OnPublished;
|
||||
contentService.CommitDocumentChanges(document);
|
||||
ContentService.Publishing -= OnPublishing;
|
||||
ContentService.Published -= OnPublished;
|
||||
try
|
||||
{
|
||||
contentService.CommitDocumentChanges(document);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ContentService.Publishing -= OnPublishing;
|
||||
ContentService.Published -= OnPublished;
|
||||
}
|
||||
|
||||
document = contentService.GetById(document.Id);
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ using Umbraco.Core.Services.Implement;
|
||||
using Umbraco.Tests.Testing;
|
||||
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
|
||||
using Umbraco.Core.Cache;
|
||||
using Umbraco.Core.PropertyEditors;
|
||||
using Umbraco.Tests.LegacyXmlPublishedCache;
|
||||
|
||||
namespace Umbraco.Tests.Services
|
||||
@@ -986,7 +987,10 @@ namespace Umbraco.Tests.Services
|
||||
Assert.IsTrue(parent.Published);
|
||||
|
||||
// content cannot publish values because they are invalid
|
||||
Assert.IsNotEmpty(content.ValidateProperties());
|
||||
var propertyValidationService = new PropertyValidationService(Factory.GetInstance<PropertyEditorCollection>(), ServiceContext.DataTypeService);
|
||||
var isValid = propertyValidationService.IsPropertyDataValid(content, out var invalidProperties);
|
||||
Assert.IsFalse(isValid);
|
||||
Assert.IsNotEmpty(invalidProperties);
|
||||
|
||||
// and therefore cannot be published,
|
||||
// because it did not have a published version at all
|
||||
|
||||
@@ -1767,7 +1767,7 @@ h1, h2, h3, h4, h5, h6, p, blockquote {
|
||||
|
||||
h1, h2, h3, h4, h5, h6
|
||||
{
|
||||
font-family:'Open Sans', sans-serif;
|
||||
font-family:'Lato', sans-serif;
|
||||
}
|
||||
|
||||
h2, h3
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Composing;
|
||||
@@ -202,6 +203,20 @@ namespace Umbraco.Tests.Strings
|
||||
Assert.AreEqual(expected, result);
|
||||
}
|
||||
|
||||
[TestCase("hello", "aGVsbG8")]
|
||||
[TestCase("tad", "dGFk")]
|
||||
[TestCase("AmqGr+Fd!~ééé", "QW1xR3IrRmQhfsOpw6nDqQ")]
|
||||
public void UrlTokenEncoding(string value, string expected)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(value);
|
||||
Console.WriteLine("base64: " + Convert.ToBase64String(bytes));
|
||||
var encoded = StringExtensions.UrlTokenEncode(bytes);
|
||||
Assert.AreEqual(expected, encoded);
|
||||
|
||||
var backBytes = StringExtensions.UrlTokenDecode(encoded);
|
||||
var backString = Encoding.UTF8.GetString(backBytes);
|
||||
Assert.AreEqual(value, backString);
|
||||
}
|
||||
|
||||
// FORMAT STRINGS
|
||||
|
||||
|
||||
@@ -243,7 +243,7 @@ namespace Umbraco.Tests.Testing
|
||||
Composition.WithCollectionBuilder<PropertyValueConverterCollectionBuilder>();
|
||||
Composition.RegisterUnique<IPublishedContentTypeFactory, PublishedContentTypeFactory>();
|
||||
|
||||
Composition.RegisterUnique<IMediaPathScheme, OriginalMediaPathScheme>();
|
||||
Composition.RegisterUnique<IMediaPathScheme, UniqueMediaPathScheme>();
|
||||
|
||||
// register empty content apps collection
|
||||
Composition.WithCollectionBuilder<ContentAppFactoryCollectionBuilder>();
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,19 +0,0 @@
|
||||
|
||||
LazyLoad.js([
|
||||
'../lib/jquery/jquery.min.js',
|
||||
'../lib/angular/angular.js',
|
||||
'../lib/underscore/underscore-min.js',
|
||||
'../lib/umbraco/Extensions.js',
|
||||
'../js/app.js',
|
||||
'../js/umbraco.resources.js',
|
||||
'../js/umbraco.services.js',
|
||||
'../js/umbraco.interceptors.js',
|
||||
'../ServerVariables',
|
||||
'../lib/signalr/jquery.signalR.js',
|
||||
'../BackOffice/signalr/hubs',
|
||||
'../js/umbraco.canvasdesigner.js'
|
||||
], function () {
|
||||
jQuery(document).ready(function () {
|
||||
angular.bootstrap(document, ['Umbraco.canvasdesigner']);
|
||||
});
|
||||
});
|
||||
+106
-80
@@ -3,8 +3,8 @@
|
||||
|
||||
function ContentEditController($rootScope, $scope, $routeParams, $q, $window,
|
||||
appState, contentResource, entityResource, navigationService, notificationsService,
|
||||
serverValidationManager, contentEditingHelper, treeService, formHelper, umbRequestHelper,
|
||||
editorState, $http, eventsService, relationResource, overlayService, $location) {
|
||||
serverValidationManager, contentEditingHelper, localizationService, formHelper, umbRequestHelper,
|
||||
editorState, $http, eventsService, overlayService, $location) {
|
||||
|
||||
var evts = [];
|
||||
var infiniteMode = $scope.infiniteModel && $scope.infiniteModel.infiniteMode;
|
||||
@@ -410,6 +410,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
/** Just shows a simple notification that there are client side validation issues to be fixed */
|
||||
function showValidationNotification() {
|
||||
//TODO: We need to make the validation UI much better, there's a lot of inconsistencies in v8 including colors, issues with the property groups and validation errors between variants
|
||||
|
||||
//need to show a notification else it's not clear there was an error.
|
||||
localizationService.localizeMany([
|
||||
"speechBubbles_validationFailedHeader",
|
||||
"speechBubbles_validationFailedMessage"
|
||||
]
|
||||
).then(function (data) {
|
||||
notificationsService.error(data[0], data[1]);
|
||||
});
|
||||
}
|
||||
|
||||
if ($scope.page.isNew) {
|
||||
|
||||
$scope.page.loading = true;
|
||||
@@ -491,7 +505,7 @@
|
||||
variants: $scope.content.variants, //set a model property for the dialog
|
||||
skipFormValidation: true, //when submitting the overlay form, skip any client side validation
|
||||
submitButtonLabelKey: "buttons_saveToPublish",
|
||||
submit: function (model) {
|
||||
submit: function(model) {
|
||||
model.submitButtonState = "busy";
|
||||
clearNotifications($scope.content);
|
||||
//we need to return this promise so that the dialog can handle the result and wire up the validation response
|
||||
@@ -499,28 +513,32 @@
|
||||
saveMethod: contentResource.sendToPublish,
|
||||
action: "sendToPublish",
|
||||
showNotifications: false
|
||||
}).then(function (data) {
|
||||
//show all notifications manually here since we disabled showing them automatically in the save method
|
||||
formHelper.showNotifications(data);
|
||||
clearNotifications($scope.content);
|
||||
overlayService.close();
|
||||
return $q.when(data);
|
||||
}, function (err) {
|
||||
clearDirtyState($scope.content.variants);
|
||||
model.submitButtonState = "error";
|
||||
//re-map the dialog model since we've re-bound the properties
|
||||
dialog.variants = $scope.content.variants;
|
||||
//don't reject, we've handled the error
|
||||
return $q.when(err);
|
||||
});
|
||||
}).then(function(data) {
|
||||
//show all notifications manually here since we disabled showing them automatically in the save method
|
||||
formHelper.showNotifications(data);
|
||||
clearNotifications($scope.content);
|
||||
overlayService.close();
|
||||
return $q.when(data);
|
||||
},
|
||||
function(err) {
|
||||
clearDirtyState($scope.content.variants);
|
||||
model.submitButtonState = "error";
|
||||
//re-map the dialog model since we've re-bound the properties
|
||||
dialog.variants = $scope.content.variants;
|
||||
//don't reject, we've handled the error
|
||||
return $q.when(err);
|
||||
});
|
||||
},
|
||||
close: function () {
|
||||
close: function() {
|
||||
overlayService.close();
|
||||
}
|
||||
};
|
||||
|
||||
overlayService.open(dialog);
|
||||
}
|
||||
else {
|
||||
showValidationNotification();
|
||||
}
|
||||
}
|
||||
else {
|
||||
$scope.page.buttonGroupState = "busy";
|
||||
@@ -547,7 +565,7 @@
|
||||
variants: $scope.content.variants, //set a model property for the dialog
|
||||
skipFormValidation: true, //when submitting the overlay form, skip any client side validation
|
||||
submitButtonLabelKey: "buttons_saveAndPublish",
|
||||
submit: function (model) {
|
||||
submit: function(model) {
|
||||
model.submitButtonState = "busy";
|
||||
clearNotifications($scope.content);
|
||||
//we need to return this promise so that the dialog can handle the result and wire up the validation response
|
||||
@@ -555,14 +573,14 @@
|
||||
saveMethod: contentResource.publish,
|
||||
action: "publish",
|
||||
showNotifications: false
|
||||
}).then(function (data) {
|
||||
//show all notifications manually here since we disabled showing them automatically in the save method
|
||||
formHelper.showNotifications(data);
|
||||
clearNotifications($scope.content);
|
||||
overlayService.close();
|
||||
return $q.when(data);
|
||||
},
|
||||
function (err) {
|
||||
}).then(function(data) {
|
||||
//show all notifications manually here since we disabled showing them automatically in the save method
|
||||
formHelper.showNotifications(data);
|
||||
clearNotifications($scope.content);
|
||||
overlayService.close();
|
||||
return $q.when(data);
|
||||
},
|
||||
function(err) {
|
||||
clearDirtyState($scope.content.variants);
|
||||
model.submitButtonState = "error";
|
||||
//re-map the dialog model since we've re-bound the properties
|
||||
@@ -571,13 +589,16 @@
|
||||
return $q.when(err);
|
||||
});
|
||||
},
|
||||
close: function () {
|
||||
close: function() {
|
||||
overlayService.close();
|
||||
}
|
||||
};
|
||||
|
||||
overlayService.open(dialog);
|
||||
}
|
||||
else {
|
||||
showValidationNotification();
|
||||
}
|
||||
}
|
||||
else {
|
||||
//ensure the flags are set
|
||||
@@ -608,7 +629,7 @@
|
||||
variants: $scope.content.variants, //set a model property for the dialog
|
||||
skipFormValidation: true, //when submitting the overlay form, skip any client side validation
|
||||
submitButtonLabelKey: "buttons_save",
|
||||
submit: function (model) {
|
||||
submit: function(model) {
|
||||
model.submitButtonState = "busy";
|
||||
clearNotifications($scope.content);
|
||||
//we need to return this promise so that the dialog can handle the result and wire up the validation response
|
||||
@@ -616,28 +637,32 @@
|
||||
saveMethod: $scope.saveMethod(),
|
||||
action: "save",
|
||||
showNotifications: false
|
||||
}).then(function (data) {
|
||||
//show all notifications manually here since we disabled showing them automatically in the save method
|
||||
formHelper.showNotifications(data);
|
||||
clearNotifications($scope.content);
|
||||
overlayService.close();
|
||||
return $q.when(data);
|
||||
}, function (err) {
|
||||
clearDirtyState($scope.content.variants);
|
||||
model.submitButtonState = "error";
|
||||
//re-map the dialog model since we've re-bound the properties
|
||||
dialog.variants = $scope.content.variants;
|
||||
//don't reject, we've handled the error
|
||||
return $q.when(err);
|
||||
});
|
||||
}).then(function(data) {
|
||||
//show all notifications manually here since we disabled showing them automatically in the save method
|
||||
formHelper.showNotifications(data);
|
||||
clearNotifications($scope.content);
|
||||
overlayService.close();
|
||||
return $q.when(data);
|
||||
},
|
||||
function(err) {
|
||||
clearDirtyState($scope.content.variants);
|
||||
model.submitButtonState = "error";
|
||||
//re-map the dialog model since we've re-bound the properties
|
||||
dialog.variants = $scope.content.variants;
|
||||
//don't reject, we've handled the error
|
||||
return $q.when(err);
|
||||
});
|
||||
},
|
||||
close: function (oldModel) {
|
||||
close: function(oldModel) {
|
||||
overlayService.close();
|
||||
}
|
||||
};
|
||||
|
||||
overlayService.open(dialog);
|
||||
}
|
||||
else {
|
||||
showValidationNotification();
|
||||
}
|
||||
}
|
||||
else {
|
||||
//ensure the flags are set
|
||||
@@ -720,6 +745,9 @@
|
||||
};
|
||||
overlayService.open(dialog);
|
||||
}
|
||||
else {
|
||||
showValidationNotification();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.publishDescendants = function() {
|
||||
@@ -776,50 +804,48 @@
|
||||
};
|
||||
overlayService.open(dialog);
|
||||
}
|
||||
else {
|
||||
showValidationNotification();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.preview = function (content) {
|
||||
// Chromes popup blocker will kick in if a window is opened
|
||||
// without the initial scoped request. This trick will fix that.
|
||||
//
|
||||
var previewWindow = $window.open('preview/?init=true', 'umbpreview');
|
||||
|
||||
// Build the correct path so both /#/ and #/ work.
|
||||
var query = 'id=' + content.id;
|
||||
if ($scope.culture) {
|
||||
query += "#?culture=" + $scope.culture;
|
||||
}
|
||||
var redirect = Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath + '/preview/?' + query;
|
||||
|
||||
if (!$scope.busy) {
|
||||
|
||||
// Chromes popup blocker will kick in if a window is opened
|
||||
// without the initial scoped request. This trick will fix that.
|
||||
//
|
||||
var previewWindow = $window.open('preview/?init=true', 'umbpreview');
|
||||
|
||||
// Build the correct path so both /#/ and #/ work.
|
||||
var query = 'id=' + content.id;
|
||||
//The user cannot save if they don't have access to do that, in which case we just want to preview
|
||||
//and that's it otherwise they'll get an unauthorized access message
|
||||
if (!_.contains(content.allowedActions, "A")) {
|
||||
previewWindow.location.href = redirect;
|
||||
}
|
||||
else {
|
||||
var selectedVariant = $scope.content.variants[0];
|
||||
if ($scope.culture) {
|
||||
query += "#?culture=" + $scope.culture;
|
||||
}
|
||||
var redirect = Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath + '/preview/?' + query;
|
||||
|
||||
//The user cannot save if they don't have access to do that, in which case we just want to preview
|
||||
//and that's it otherwise they'll get an unauthorized access message
|
||||
if (!_.contains(content.allowedActions, "A")) {
|
||||
previewWindow.location.href = redirect;
|
||||
}
|
||||
else {
|
||||
var selectedVariant = $scope.content.variants[0];
|
||||
if ($scope.culture) {
|
||||
var found = _.find($scope.content.variants, function (v) {
|
||||
return (v.language && v.language.culture === $scope.culture);
|
||||
});
|
||||
|
||||
if(found){
|
||||
selectedVariant = found;
|
||||
}
|
||||
}
|
||||
|
||||
//ensure the save flag is set
|
||||
selectedVariant.save = true;
|
||||
performSave({ saveMethod: $scope.saveMethod(), action: "save" }).then(function (data) {
|
||||
previewWindow.location.href = redirect;
|
||||
}, function (err) {
|
||||
//validation issues ....
|
||||
var found = _.find($scope.content.variants, function (v) {
|
||||
return (v.language && v.language.culture === $scope.culture);
|
||||
});
|
||||
|
||||
if (found) {
|
||||
selectedVariant = found;
|
||||
}
|
||||
}
|
||||
|
||||
//ensure the save flag is set
|
||||
selectedVariant.save = true;
|
||||
performSave({ saveMethod: $scope.saveMethod(), action: "save" }).then(function (data) {
|
||||
previewWindow.location.href = redirect;
|
||||
}, function (err) {
|
||||
//validation issues ....
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+23
-1
@@ -46,7 +46,7 @@ Use this directive to generate a list of breadcrumbs.
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
function BreadcrumbsDirective() {
|
||||
function BreadcrumbsDirective($location, navigationService) {
|
||||
|
||||
function link(scope, el, attr, ctrl) {
|
||||
|
||||
@@ -58,6 +58,28 @@ Use this directive to generate a list of breadcrumbs.
|
||||
}
|
||||
};
|
||||
|
||||
scope.openPath = function (ancestor, event) {
|
||||
// targeting a new tab/window?
|
||||
if (event.ctrlKey ||
|
||||
event.shiftKey ||
|
||||
event.metaKey || // apple
|
||||
(event.button && event.button === 1) // middle click, >IE9 + everyone else
|
||||
) {
|
||||
// yes, let the link open itself
|
||||
return;
|
||||
}
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
var path = scope.pathTo(ancestor);
|
||||
$location.path(path);
|
||||
navigationService.clearSearch(["cculture"]);
|
||||
}
|
||||
|
||||
scope.pathTo = function (ancestor) {
|
||||
return "/" + scope.entityType + "/" + scope.entityType + "/edit/" + ancestor.id;
|
||||
}
|
||||
|
||||
function onInit() {
|
||||
if ("onOpen" in attr) {
|
||||
scope.allowOnOpen = true;
|
||||
|
||||
+10
-1
@@ -34,8 +34,17 @@ Use this directive to generate color swatches to pick from.
|
||||
}
|
||||
|
||||
scope.setColor = function (color, $index, $event) {
|
||||
scope.selectedColor = color;
|
||||
if (scope.onSelect) {
|
||||
// did the value change?
|
||||
if (scope.selectedColor != null && scope.selectedColor.value === color.value) {
|
||||
// User clicked the currently selected color
|
||||
// to remove the selection, they don't want
|
||||
// to select any color after all.
|
||||
// Unselect the color
|
||||
color = null;
|
||||
}
|
||||
|
||||
scope.selectedColor = color;
|
||||
scope.onSelect({color: color, $index: $index, $event: $event});
|
||||
$event.stopPropagation();
|
||||
}
|
||||
|
||||
+2
-2
@@ -49,11 +49,11 @@ When this combination is hit an overview is opened with shortcuts based on the m
|
||||
"name": "Design",
|
||||
"shortcuts": [
|
||||
{
|
||||
"description": "Add tab",
|
||||
"description": "Add group",
|
||||
"keys": [
|
||||
{"key": "alt"},
|
||||
{"key": "shift"},
|
||||
{"key": "t"}
|
||||
{"key": "g"}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -3,23 +3,29 @@
|
||||
|
||||
/**
|
||||
* Used to set the current client culture on all requests API requests
|
||||
* @param {any} $q
|
||||
* @param {any} $routeParams
|
||||
*/
|
||||
function cultureRequestInterceptor($q, $routeParams) {
|
||||
function cultureRequestInterceptor($injector) {
|
||||
return {
|
||||
//dealing with requests:
|
||||
'request': function (config) {
|
||||
|
||||
if (!Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath) {
|
||||
// no settings available, we're probably on the login screen
|
||||
return config;
|
||||
}
|
||||
|
||||
if (!config.url.match(RegExp(Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath + "\/backoffice\/", "i"))) {
|
||||
// it's not an API request, no handling
|
||||
return config;
|
||||
}
|
||||
// it's an API request, add the current client culture as a header value
|
||||
config.headers["X-UMB-CULTURE"] = $routeParams.cculture ? $routeParams.cculture : $routeParams.mculture;
|
||||
|
||||
var $routeParams = $injector.get("$routeParams");
|
||||
if ($routeParams) {
|
||||
// it's an API request, add the current client culture as a header value
|
||||
config.headers["X-UMB-CULTURE"] = $routeParams.cculture ? $routeParams.cculture : $routeParams.mculture;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -28,7 +28,7 @@ function navigationService($routeParams, $location, $q, $timeout, $injector, eve
|
||||
|
||||
//A list of query strings defined that when changed will not cause a reload of the route
|
||||
var nonRoutingQueryStrings = ["mculture", "cculture", "lq"];
|
||||
var retainedQueryStrings = ["mculture", "cculture"];
|
||||
var retainedQueryStrings = ["mculture"];
|
||||
|
||||
|
||||
function setMode(mode) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div ng-controller="Umbraco.Install.UserController">
|
||||
<h1>Install Umbraco 8 Preview <span style="font-weight: normal; font-size: 50%">(Editor Track - <a style="text-decoration: underline; color: blue" href="https://github.com/umbraco/Umbraco-CMS/blob/temp8/.github/README.md#when-is-umbraco-8-coming" target="_blank">what does that mean</a>?)</span></h1>
|
||||
<h1>Install Umbraco 8</h1>
|
||||
|
||||
<p>Enter your name, email and password to install Umbraco 8 with its default settings, alternatively you can customize your installation</p>
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ body {
|
||||
position: absolute;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-family: "Lato", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #343434;
|
||||
|
||||
@@ -82,6 +82,10 @@
|
||||
.umb-editor-header__name-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
border: 1px solid @inputBorder;
|
||||
&:focus-within {
|
||||
border-color: @inputBorderFocus;
|
||||
}
|
||||
}
|
||||
|
||||
.umb-editor-header__name-and-description {
|
||||
@@ -109,7 +113,9 @@ input.umb-editor-header__name-input {
|
||||
width: 100%;
|
||||
padding: 0 10px;
|
||||
background: @white;
|
||||
border: 1px solid @gray-8;
|
||||
&:focus {
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
input.umb-editor-header__name-input:disabled {
|
||||
@@ -156,7 +162,6 @@ a.umb-editor-header__close-split-view:hover {
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
margin: 1px 1px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
height: 30px;
|
||||
text-decoration: none !important;
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
*/
|
||||
@fontPath: '../fonts';
|
||||
@latoPath: '@{fontPath}/lato';
|
||||
@openSansPath: '@{fontPath}/opensans';
|
||||
|
||||
/* Webfont: LatoLatin-Black */
|
||||
@font-face {
|
||||
@@ -114,34 +113,3 @@
|
||||
font-display: swap;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Open Sans
|
||||
|
||||
Normal: font-weight: 400;
|
||||
Semi-bold: font-weight: 600;
|
||||
|
||||
*/
|
||||
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
src: local('Open Sans'),
|
||||
local('OpenSans'),
|
||||
url('@{openSansPath}/OpenSans-Regular.woff2') format('woff2'), /* Super Modern Browsers */
|
||||
url('@{openSansPath}/OpenSans-Regular.woff') format('woff'); /* Modern Browsers */
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
src: local('Open Sans'),
|
||||
local('OpenSans'),
|
||||
url('@{openSansPath}/OpenSans-Semibold.woff2') format('woff2'), /* Super Modern Browsers */
|
||||
url('@{openSansPath}/OpenSans-Semibold.woff') format('woff'); /* Modern Browsers */
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@@ -41,8 +41,8 @@
|
||||
@purple-washed: #F6F3FD;
|
||||
|
||||
// UI Colors
|
||||
@red-d1: #F02E28;
|
||||
@red: #D42054;// updated 2019
|
||||
@red-d1: #F02E28;// currently used for validation, and is hard coded inline in various html places :/
|
||||
@red: #D42054;// updated 2019 - should be used as validation! and is already in some cases like the .umb-validation-label
|
||||
@red-l1: #e22c60;// updated 2019
|
||||
@red-l2: #FE8B88;
|
||||
@red-l3: #FFB2B0;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<li class="umb-breadcrumbs__ancestor" ng-repeat="ancestor in ancestors">
|
||||
|
||||
<!-- go to node on click -->
|
||||
<a ng-if="!$last && !allowOnOpen" href="#/{{entityType}}/{{entityType}}/edit/{{ancestor.id}}" class="umb-breadcrumbs__ancestor-link" title="{{ancestor.name}}">{{ancestor.name}}</a>
|
||||
<a ng-if="!$last && !allowOnOpen" ng-href="#{{::pathTo(ancestor)}}" ng-click="openPath(ancestor, $event)" class="umb-breadcrumbs__ancestor-link" title="{{ancestor.name}}">{{ancestor.name}}</a>
|
||||
|
||||
<!-- use callback to handle click -->
|
||||
<a ng-if="!$last && allowOnOpen" href="#" ng-click="open(ancestor)" class="umb-breadcrumbs__ancestor-link" title="{{ancestor.name}}" prevent-default>{{ancestor.name}}</a>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
</ng-form>
|
||||
|
||||
<div id="nameField" class="umb-editor-header__name-and-description" style="flex: 1 1 auto;">
|
||||
<div class="umb-editor-header__name-wrapper">
|
||||
<div class="umb-editor-header__name-wrapper" ng-show="!nameLocked || !hideAlias">
|
||||
<ng-form name="headerNameForm">
|
||||
<input data-element="editor-name-field"
|
||||
no-password-manager
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
class="typeahead tags-{{vm.htmlId}}"
|
||||
ng-model="vm.tagToAdd"
|
||||
ng-keydown="vm.addTagOnEnter($event)"
|
||||
on-blur="vm.addTag()"
|
||||
ng-blur="vm.addTag()"
|
||||
localize="placeholder"
|
||||
placeholder="@placeholders_enterTags" />
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<i class="umb-grid-selector__item-remove icon-trash" ng-click="removeItem(selectedItem)"></i>
|
||||
</div>
|
||||
|
||||
<a href="" class="umb-grid-selector__item -placeholder" ng-if="(availableItems | compareArrays:selectedItems:'alias').length > 0" ng-click="openItemPicker($event)" hotkey="alt+shift+t">
|
||||
<a href="" class="umb-grid-selector__item -placeholder" ng-if="(availableItems | compareArrays:selectedItems:'alias').length > 0" ng-click="openItemPicker($event)" hotkey="alt+shift+g">
|
||||
<div class="umb-grid-selector__item-content">
|
||||
<div class="umb-grid-selector__item-label -blue" ng-if="defaultItem !== null"><localize key="grid_chooseExtra">Choose extra</localize> {{ itemLabel }}</div>
|
||||
<div class="umb-grid-selector__item-label -blue" ng-if="defaultItem === null"><localize key="grid_chooseDefault">Choose default</localize> {{ itemLabel }}</div>
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
</umb-editor-sub-header>
|
||||
|
||||
<div ng-if="sortingMode && model.groups.length <= 1" class="umb-group-builder__no-data-text">
|
||||
<localize key="contentTypeEditor_noTabs"></localize>
|
||||
<localize key="contentTypeEditor_noGroups"></localize>
|
||||
</div>
|
||||
|
||||
<ul class="umb-group-builder__groups" ui-sortable="sortableOptionsGroup" ng-model="model.groups">
|
||||
@@ -41,7 +41,7 @@
|
||||
<li ng-repeat="tab in model.groups" ng-class="{'umb-group-builder__group-sortable': sortingMode}" data-element="group-{{tab.name}}">
|
||||
|
||||
<!-- TAB INIT STATE -->
|
||||
<a href="" class="umb-group-builder__group -placeholder" hotkey="alt+shift+t" ng-click="addGroup(tab)" ng-if="tab.tabState=='init' && !sortingMode" data-element="group-add">
|
||||
<a href="" class="umb-group-builder__group -placeholder" hotkey="alt+shift+g" ng-click="addGroup(tab)" ng-if="tab.tabState=='init' && !sortingMode" data-element="group-add">
|
||||
<localize key="contentTypeEditor_addGroup"></localize>
|
||||
</a>
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"treeHeaders_templates",
|
||||
"main_sections",
|
||||
"shortcuts_navigateSections",
|
||||
"shortcuts_addTab",
|
||||
"shortcuts_addGroup",
|
||||
"shortcuts_addProperty",
|
||||
"shortcuts_addEditor",
|
||||
"shortcuts_editDataType",
|
||||
@@ -74,7 +74,7 @@
|
||||
// keyboard shortcuts
|
||||
vm.labels.sections = values[4];
|
||||
vm.labels.navigateSections = values[5];
|
||||
vm.labels.addTab = values[6];
|
||||
vm.labels.addGroup = values[6];
|
||||
vm.labels.addProperty = values[7];
|
||||
vm.labels.addEditor = values[8];
|
||||
vm.labels.editDataType = values[9];
|
||||
@@ -127,8 +127,8 @@
|
||||
"name": vm.labels.design,
|
||||
"shortcuts": [
|
||||
{
|
||||
"description": vm.labels.addTab,
|
||||
"keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "t" }]
|
||||
"description": vm.labels.addGroup,
|
||||
"keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "g" }]
|
||||
},
|
||||
{
|
||||
"description": vm.labels.addProperty,
|
||||
@@ -497,13 +497,17 @@
|
||||
});
|
||||
|
||||
// #3368 - changes on the other "buttons" do not register on the current form, so we manually have to flag the form as dirty
|
||||
$scope.$watch("vm.contentType.allowedContentTypes.length + vm.contentType.allowAsRoot + vm.contentType.allowedTemplates.length + vm.contentType.isContainer", function (newVal, oldVal) {
|
||||
if (oldVal === undefined) {
|
||||
// still initializing, ignore
|
||||
return;
|
||||
$scope.$watch(
|
||||
"vm.contentType.allowedContentTypes.length + vm.contentType.allowAsRoot + vm.contentType.allowCultureVariant + vm.contentType.isElement + " +
|
||||
"vm.contentType.allowedTemplates.length + vm.contentType.isContainer + vm.contentType.compositeContentTypes.length",
|
||||
function(newVal, oldVal) {
|
||||
if (oldVal === undefined) {
|
||||
// still initializing, ignore
|
||||
return;
|
||||
}
|
||||
angularHelper.getCurrentForm($scope).$setDirty();
|
||||
}
|
||||
angularHelper.getCurrentForm($scope).$setDirty();
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
angular.module("umbraco").controller("Umbraco.Editors.DocumentTypes.EditController", DocumentTypesEditController);
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* The controller for the media editor
|
||||
*/
|
||||
function mediaEditController($scope, $routeParams, $q, appState, mediaResource,
|
||||
entityResource, navigationService, notificationsService, angularHelper,
|
||||
entityResource, navigationService, notificationsService, localizationService,
|
||||
serverValidationManager, contentEditingHelper, fileManager, formHelper,
|
||||
editorState, umbRequestHelper, $http, eventsService) {
|
||||
|
||||
@@ -155,12 +155,25 @@ function mediaEditController($scope, $routeParams, $q, appState, mediaResource,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Just shows a simple notification that there are client side validation issues to be fixed */
|
||||
function showValidationNotification() {
|
||||
//TODO: We need to make the validation UI much better, there's a lot of inconsistencies in v8 including colors, issues with the property groups and validation errors between variants
|
||||
|
||||
//need to show a notification else it's not clear there was an error.
|
||||
localizationService.localizeMany([
|
||||
"speechBubbles_validationFailedHeader",
|
||||
"speechBubbles_validationFailedMessage"
|
||||
]
|
||||
).then(function (data) {
|
||||
notificationsService.error(data[0], data[1]);
|
||||
});
|
||||
}
|
||||
|
||||
$scope.save = function () {
|
||||
|
||||
if (!$scope.busy && formHelper.submitForm({ scope: $scope })) {
|
||||
|
||||
$scope.busy = true;
|
||||
if (formHelper.submitForm({ scope: $scope })) {
|
||||
|
||||
$scope.page.saveButtonState = "busy";
|
||||
|
||||
mediaResource.save($scope.content, create, fileManager.getFiles())
|
||||
@@ -176,7 +189,6 @@ function mediaEditController($scope, $routeParams, $q, appState, mediaResource,
|
||||
});
|
||||
|
||||
editorState.set($scope.content);
|
||||
$scope.busy = false;
|
||||
|
||||
syncTreeNode($scope.content, data.path);
|
||||
|
||||
@@ -199,12 +211,12 @@ function mediaEditController($scope, $routeParams, $q, appState, mediaResource,
|
||||
});
|
||||
|
||||
editorState.set($scope.content);
|
||||
$scope.busy = false;
|
||||
$scope.page.saveButtonState = "error";
|
||||
|
||||
});
|
||||
}else{
|
||||
$scope.busy = false;
|
||||
}
|
||||
else {
|
||||
showValidationNotification();
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
function MediaTypesEditController($scope, $routeParams, mediaTypeResource,
|
||||
dataTypeResource, editorState, contentEditingHelper, formHelper,
|
||||
navigationService, iconHelper, contentTypeHelper, notificationsService,
|
||||
$filter, $q, localizationService, overlayHelper, eventsService) {
|
||||
$filter, $q, localizationService, overlayHelper, eventsService, angularHelper) {
|
||||
|
||||
var vm = this;
|
||||
var evts = [];
|
||||
@@ -51,7 +51,7 @@
|
||||
|
||||
"main_sections",
|
||||
"shortcuts_navigateSections",
|
||||
"shortcuts_addTab",
|
||||
"shortcuts_addGroup",
|
||||
"shortcuts_addProperty",
|
||||
"shortcuts_addEditor",
|
||||
"shortcuts_editDataType",
|
||||
@@ -68,7 +68,7 @@
|
||||
// keyboard shortcuts
|
||||
vm.labels.sections = values[3];
|
||||
vm.labels.navigateSections = values[4];
|
||||
vm.labels.addTab = values[5];
|
||||
vm.labels.addGroup = values[5];
|
||||
vm.labels.addProperty = values[6];
|
||||
vm.labels.addEditor = values[7];
|
||||
vm.labels.editDataType = values[8];
|
||||
@@ -113,8 +113,8 @@
|
||||
"name": vm.labels.design,
|
||||
"shortcuts": [
|
||||
{
|
||||
"description": vm.labels.addTab,
|
||||
"keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "t" }]
|
||||
"description": vm.labels.addGroup,
|
||||
"keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "g" }]
|
||||
},
|
||||
{
|
||||
"description": vm.labels.addProperty,
|
||||
@@ -419,6 +419,15 @@
|
||||
eventsService.unsubscribe(evts[e]);
|
||||
}
|
||||
});
|
||||
|
||||
// changes on the other "buttons" do not register on the current form, so we manually have to flag the form as dirty
|
||||
$scope.$watch("vm.contentType.allowedContentTypes.length + vm.contentType.allowAsRoot + vm.contentType.isContainer + vm.contentType.compositeContentTypes.length", function (newVal, oldVal) {
|
||||
if (oldVal === undefined) {
|
||||
// still initializing, ignore
|
||||
return;
|
||||
}
|
||||
angularHelper.getCurrentForm($scope).$setDirty();
|
||||
});
|
||||
}
|
||||
|
||||
angular.module("umbraco").controller("Umbraco.Editors.MediaTypes.EditController", MediaTypesEditController);
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* @description
|
||||
* The controller for the member editor
|
||||
*/
|
||||
function MemberEditController($scope, $routeParams, $location, $q, $window, appState, memberResource, entityResource, navigationService, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, fileManager, formHelper, umbModelMapper, editorState, umbRequestHelper, $http) {
|
||||
function MemberEditController($scope, $routeParams, $location, appState, memberResource, entityResource, navigationService, notificationsService, localizationService, serverValidationManager, contentEditingHelper, fileManager, formHelper, editorState, umbRequestHelper, $http) {
|
||||
|
||||
//setup scope vars
|
||||
$scope.page = {};
|
||||
@@ -18,7 +18,6 @@ function MemberEditController($scope, $routeParams, $location, $q, $window, appS
|
||||
$scope.page.listViewPath = null;
|
||||
$scope.page.saveButtonState = "init";
|
||||
$scope.page.exportButton = "init";
|
||||
$scope.busy = false;
|
||||
|
||||
$scope.page.listViewPath = ($routeParams.page && $routeParams.listName)
|
||||
? "/member/member/list/" + $routeParams.listName + "?page=" + $routeParams.page
|
||||
@@ -131,14 +130,26 @@ function MemberEditController($scope, $routeParams, $location, $q, $window, appS
|
||||
if(content.membershipScenario === 0) {
|
||||
$scope.page.nameLocked = true;
|
||||
}
|
||||
}
|
||||
|
||||
/** Just shows a simple notification that there are client side validation issues to be fixed */
|
||||
function showValidationNotification() {
|
||||
//TODO: We need to make the validation UI much better, there's a lot of inconsistencies in v8 including colors, issues with the property groups and validation errors between variants
|
||||
|
||||
//need to show a notification else it's not clear there was an error.
|
||||
localizationService.localizeMany([
|
||||
"speechBubbles_validationFailedHeader",
|
||||
"speechBubbles_validationFailedMessage"
|
||||
]
|
||||
).then(function (data) {
|
||||
notificationsService.error(data[0], data[1]);
|
||||
});
|
||||
}
|
||||
|
||||
$scope.save = function() {
|
||||
|
||||
if (!$scope.busy && formHelper.submitForm({ scope: $scope })) {
|
||||
if (formHelper.submitForm({ scope: $scope })) {
|
||||
|
||||
$scope.busy = true;
|
||||
$scope.page.saveButtonState = "busy";
|
||||
|
||||
memberResource.save($scope.content, $routeParams.create, fileManager.getFiles())
|
||||
@@ -155,7 +166,6 @@ function MemberEditController($scope, $routeParams, $location, $q, $window, appS
|
||||
});
|
||||
|
||||
editorState.set($scope.content);
|
||||
$scope.busy = false;
|
||||
$scope.page.saveButtonState = "success";
|
||||
|
||||
var path = buildTreePath(data);
|
||||
@@ -172,12 +182,12 @@ function MemberEditController($scope, $routeParams, $location, $q, $window, appS
|
||||
});
|
||||
|
||||
editorState.set($scope.content);
|
||||
$scope.busy = false;
|
||||
$scope.page.saveButtonState = "error";
|
||||
|
||||
});
|
||||
}else{
|
||||
$scope.busy = false;
|
||||
}
|
||||
else {
|
||||
showValidationNotification();
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
var labelKeys = [
|
||||
"general_design",
|
||||
"shortcuts_shortcut",
|
||||
"shortcuts_addTab",
|
||||
"shortcuts_addGroup",
|
||||
"shortcuts_addProperty",
|
||||
"shortcuts_addEditor",
|
||||
"shortcuts_editDataType"
|
||||
@@ -35,7 +35,7 @@
|
||||
|
||||
vm.labels.design = values[0];
|
||||
vm.labels.shortcut = values[1];
|
||||
vm.labels.addTab = values[2];
|
||||
vm.labels.addGroup = values[2];
|
||||
vm.labels.addProperty = values[3];
|
||||
vm.labels.addEditor = values[4];
|
||||
vm.labels.editDataType = values[5];
|
||||
@@ -54,8 +54,8 @@
|
||||
"name": vm.labels.shortcut,
|
||||
"shortcuts": [
|
||||
{
|
||||
"description": vm.labels.addTab,
|
||||
"keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "t" }]
|
||||
"description": vm.labels.addGroup,
|
||||
"keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "g" }]
|
||||
},
|
||||
{
|
||||
"description": vm.labels.addProperty,
|
||||
|
||||
+3
-33
@@ -9,8 +9,7 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
function ListViewGridLayoutController($scope, $routeParams, mediaHelper, mediaResource,
|
||||
listViewHelper, mediaTypeHelper, editorService, $location) {
|
||||
function ListViewGridLayoutController($scope, $routeParams, mediaHelper, mediaResource, $location, listViewHelper, mediaTypeHelper) {
|
||||
|
||||
var vm = this;
|
||||
var umbracoSettings = Umbraco.Sys.ServerVariables.umbracoSettings;
|
||||
@@ -117,37 +116,8 @@
|
||||
listViewHelper.selectHandler(folder, $index, $scope.folders, $scope.selection, $event);
|
||||
}
|
||||
|
||||
function goToItem(node, $event, $index) {
|
||||
$event.stopPropagation();
|
||||
|
||||
if ($scope.entityType === "content") {
|
||||
|
||||
var contentEditor = {
|
||||
id: node.id,
|
||||
submit: function (model) {
|
||||
// update the node
|
||||
node.name = model.contentNode.name;
|
||||
// TODO: node.description = model.contentNode.description;
|
||||
node.published = model.contentNode.hasPublishedVersion;
|
||||
if (entityType !== "Member") {
|
||||
entityResource.getUrl(model.contentNode.id, entityType).then(function (data) {
|
||||
node.url = data;
|
||||
});
|
||||
}
|
||||
editorService.close();
|
||||
},
|
||||
close: function () {
|
||||
editorService.close();
|
||||
}
|
||||
};
|
||||
editorService.contentEditor(contentEditor);
|
||||
|
||||
} else {
|
||||
|
||||
// if node.id is 2147483647 (int.MaxValue) use node.key
|
||||
$location.path($scope.entityType + '/' + $scope.entityType + '/edit/' + (node.id === 2147483647 ? node.key : node.id));
|
||||
|
||||
}
|
||||
function goToItem(item, $event, $index) {
|
||||
$location.path($scope.entityType + '/' + $scope.entityType + '/edit/' + item.id);
|
||||
}
|
||||
|
||||
activate();
|
||||
|
||||
+5
-32
@@ -1,7 +1,7 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
function ListViewListLayoutController($scope, listViewHelper, mediaHelper, mediaTypeHelper, editorService, $location) {
|
||||
function ListViewListLayoutController($scope, listViewHelper, $location, mediaHelper, mediaTypeHelper) {
|
||||
|
||||
var vm = this;
|
||||
var umbracoSettings = Umbraco.Sys.ServerVariables.umbracoSettings;
|
||||
@@ -52,37 +52,10 @@
|
||||
listViewHelper.selectHandler(selectedItem, $index, $scope.items, $scope.selection, $event);
|
||||
}
|
||||
|
||||
function clickItem(node) {
|
||||
|
||||
if ($scope.entityType === "content") {
|
||||
|
||||
var contentEditor = {
|
||||
id: node.id,
|
||||
submit: function (model) {
|
||||
// update the node
|
||||
node.name = model.contentNode.name;
|
||||
// TODO: node.description = model.contentNode.description;
|
||||
node.published = model.contentNode.hasPublishedVersion;
|
||||
if (entityType !== "Member") {
|
||||
entityResource.getUrl(model.contentNode.id, entityType).then(function (data) {
|
||||
node.url = data;
|
||||
});
|
||||
}
|
||||
editorService.close();
|
||||
},
|
||||
close: function () {
|
||||
editorService.close();
|
||||
}
|
||||
};
|
||||
editorService.contentEditor(contentEditor);
|
||||
|
||||
} else {
|
||||
// if node.id is 2147483647 (int.MaxValue) use node.key
|
||||
$location.path($scope.entityType + '/' + $scope.entityType + '/edit/' + (node.id === 2147483647 ? node.key : node.id));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function clickItem(item) {
|
||||
// if item.id is 2147483647 (int.MaxValue) use item.key
|
||||
$location.path($scope.entityType + '/' + $scope.entityType + '/edit/' + (item.id === 2147483647 ? item.key : item.id));
|
||||
}
|
||||
|
||||
function isSortDirection(col, direction) {
|
||||
return listViewHelper.setSortingDirection(col, direction, $scope.options);
|
||||
|
||||
@@ -105,7 +105,7 @@
|
||||
<PackageReference Include="MiniProfiler" Version="4.0.138" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
|
||||
<PackageReference Include="Umbraco.ModelsBuilder.Ui">
|
||||
<Version>8.0.0</Version>
|
||||
<Version>8.0.1</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Umbraco.SqlServerCE" Version="4.0.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -702,7 +702,7 @@
|
||||
<key alias="blue">Blå</key>
|
||||
</area>
|
||||
<area alias="shortcuts">
|
||||
<key alias="addTab">Tilføj fane</key>
|
||||
<key alias="addGroup">Tilføj fane</key>
|
||||
<key alias="addProperty">Tilføj egenskab</key>
|
||||
<key alias="addEditor">Tilføj editor</key>
|
||||
<key alias="addTemplate">Tilføj skabelon</key>
|
||||
@@ -1276,7 +1276,7 @@ Mange hilsner fra Umbraco robotten
|
||||
</area>
|
||||
<area alias="contentTypeEditor">
|
||||
<key alias="compositions">Kompositioner</key>
|
||||
<key alias="noTabs">Du har ikke tilføjet nogle faner</key>
|
||||
<key alias="noGroups">Du har ikke tilføjet nogle grupper</key>
|
||||
<key alias="addGroup">Tilføj gruppe</key>
|
||||
<key alias="inheritedFrom">Nedarvet fra</key>
|
||||
<key alias="addProperty">Tilføj egenskab</key>
|
||||
|
||||
@@ -733,7 +733,7 @@
|
||||
<key alias="blue">Blue</key>
|
||||
</area>
|
||||
<area alias="shortcuts">
|
||||
<key alias="addTab">Add tab</key>
|
||||
<key alias="addGroup">Add group</key>
|
||||
<key alias="addProperty">Add property</key>
|
||||
<key alias="addEditor">Add editor</key>
|
||||
<key alias="addTemplate">Add template</key>
|
||||
@@ -1542,7 +1542,7 @@ To manage your website, simply open the Umbraco back office and start adding con
|
||||
</area>
|
||||
<area alias="contentTypeEditor">
|
||||
<key alias="compositions">Compositions</key>
|
||||
<key alias="noTabs">You have not added any tabs</key>
|
||||
<key alias="noGroups">You have not added any groups</key>
|
||||
<key alias="addGroup">Add group</key>
|
||||
<key alias="inheritedFrom">Inherited from</key>
|
||||
<key alias="addProperty">Add property</key>
|
||||
|
||||
@@ -735,7 +735,7 @@
|
||||
<key alias="blue">Blue</key>
|
||||
</area>
|
||||
<area alias="shortcuts">
|
||||
<key alias="addTab">Add tab</key>
|
||||
<key alias="addGroup">Add group</key>
|
||||
<key alias="addProperty">Add property</key>
|
||||
<key alias="addEditor">Add editor</key>
|
||||
<key alias="addTemplate">Add template</key>
|
||||
@@ -1552,7 +1552,7 @@ To manage your website, simply open the Umbraco back office and start adding con
|
||||
</area>
|
||||
<area alias="contentTypeEditor">
|
||||
<key alias="compositions">Compositions</key>
|
||||
<key alias="noTabs">You have not added any tabs</key>
|
||||
<key alias="noGroups">You have not added any groups</key>
|
||||
<key alias="addGroup">Add group</key>
|
||||
<key alias="inheritedFrom">Inherited from</key>
|
||||
<key alias="addProperty">Add property</key>
|
||||
|
||||
@@ -597,7 +597,7 @@
|
||||
<key alias="blue">Azul</key>
|
||||
</area>
|
||||
<area alias="shortcuts">
|
||||
<key alias="addTab">Añadir pestaña</key>
|
||||
<key alias="addGroup">Añadir pestaña</key>
|
||||
<key alias="addProperty">Añadir propiedad</key>
|
||||
<key alias="addEditor">Añadir editor</key>
|
||||
<key alias="addTemplate">Añadir platilla</key>
|
||||
@@ -1200,7 +1200,7 @@
|
||||
</area>
|
||||
<area alias="contentTypeEditor">
|
||||
<key alias="compositions">Composiciones</key>
|
||||
<key alias="noTabs">No has añadido ninguna pestaña</key>
|
||||
<key alias="noGroups">No has añadido ninguna pestaña</key>
|
||||
<key alias="inheritedFrom">Heredado de</key>
|
||||
<key alias="addProperty">Añadir propiedad</key>
|
||||
<key alias="requiredLabel">Etiqueta requerida</key>
|
||||
|
||||
@@ -612,7 +612,7 @@
|
||||
<key alias="blue">Bleu</key>
|
||||
</area>
|
||||
<area alias="shortcuts">
|
||||
<key alias="addTab">Ajouter un onglet</key>
|
||||
<key alias="addGroup">Ajouter un onglet</key>
|
||||
<key alias="addProperty">Ajouter une propriété</key>
|
||||
<key alias="addEditor">Ajouter un éditeur</key>
|
||||
<key alias="addTemplate">Ajouter un modèle</key>
|
||||
@@ -1374,7 +1374,7 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à
|
||||
</area>
|
||||
<area alias="contentTypeEditor">
|
||||
<key alias="compositions">Compositions</key>
|
||||
<key alias="noTabs">Vous n'avez pas ajouté d'onglet</key>
|
||||
<key alias="noGroups">Vous n'avez pas ajouté d'onglet</key>
|
||||
<key alias="inheritedFrom">Hérité de</key>
|
||||
<key alias="addProperty">Ajouter une propriété</key>
|
||||
<key alias="requiredLabel">Label requis</key>
|
||||
|
||||
@@ -466,7 +466,7 @@
|
||||
<key alias="blue">ブルー</key>
|
||||
</area>
|
||||
<area alias="shortcuts">
|
||||
<key alias="addTab">タブの追加</key>
|
||||
<key alias="addGroup">タブの追加</key>
|
||||
<key alias="addProperty">プロパティの追加</key>
|
||||
<key alias="addEditor">エディターの追加</key>
|
||||
<key alias="addTemplate">テンプレートの追加</key>
|
||||
@@ -932,7 +932,7 @@ Runwayをインストールして作られた新しいウェブサイトがど
|
||||
</area>
|
||||
<area alias="contentTypeEditor">
|
||||
<key alias="compositions">構成</key>
|
||||
<key alias="noTabs">タブが追加されていません</key>
|
||||
<key alias="noGroups">タブが追加されていません</key>
|
||||
<key alias="inheritedFrom">次から継承:</key>
|
||||
<key alias="addProperty">プロパティの追加</key>
|
||||
<key alias="requiredLabel">必要なラベル</key>
|
||||
|
||||
@@ -505,7 +505,7 @@
|
||||
<key alias="blue">Blauw</key>
|
||||
</area>
|
||||
<area alias="shortcuts">
|
||||
<key alias="addTab">Tab toevoegen</key>
|
||||
<key alias="addGroup">Groep toevoegen</key>
|
||||
<key alias="addProperty">Property toevoegen</key>
|
||||
<key alias="addEditor">Editor toevoegen</key>
|
||||
<key alias="addTemplate">Template toevoegen</key>
|
||||
@@ -968,7 +968,8 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je
|
||||
</area>
|
||||
<area alias="contentTypeEditor">
|
||||
<key alias="compositions">Composities</key>
|
||||
<key alias="noTabs">Er zijn nog geen tabs toegevoegd</key>
|
||||
<key alias="noGroups">Er zijn nog geen groepen toegevoegd</key>
|
||||
<key alias="addGroup">Voeg groep toe</key>
|
||||
<key alias="inheritedFrom">Inherited van</key>
|
||||
<key alias="addProperty">Voeg property toe</key>
|
||||
<key alias="requiredLabel">Verplicht label</key>
|
||||
|
||||
@@ -579,7 +579,7 @@
|
||||
<key alias="blue">Niebieski</key>
|
||||
</area>
|
||||
<area alias="shortcuts">
|
||||
<key alias="addTab">Dodaj zakładkę</key>
|
||||
<key alias="addGroup">Dodaj zakładkę</key>
|
||||
<key alias="addProperty">Dodaj właściwość</key>
|
||||
<key alias="addEditor">Dodaj edytora</key>
|
||||
<key alias="addTemplate">Dodaj szablon</key>
|
||||
@@ -1180,7 +1180,7 @@ Naciśnij przycisk <strong>instaluj</strong>, aby zainstalować bazę danych Umb
|
||||
</area>
|
||||
<area alias="contentTypeEditor">
|
||||
<key alias="compositions">Kompozycje</key>
|
||||
<key alias="noTabs">Nie dodałeś żadnych zakładek</key>
|
||||
<key alias="noGroups">Nie dodałeś żadnych zakładek</key>
|
||||
<key alias="inheritedFrom">Odziedziczone z</key>
|
||||
<key alias="addProperty">Dodaj właściwość</key>
|
||||
<key alias="requiredLabel">Wymagana etykieta</key>
|
||||
|
||||
@@ -269,7 +269,7 @@
|
||||
</area>
|
||||
<area alias="contentTypeEditor">
|
||||
<key alias="compositions">Композиции</key>
|
||||
<key alias="noTabs">Вы не добавили ни одной вкладки</key>
|
||||
<key alias="noGroups">Вы не добавили ни одной вкладки</key>
|
||||
<key alias="inheritedFrom">Унаследовано от</key>
|
||||
<key alias="addProperty">Добавить свойство</key>
|
||||
<key alias="requiredLabel">Обязательная метка</key>
|
||||
@@ -1355,7 +1355,7 @@
|
||||
<key alias="tabs">Вкладки</key>
|
||||
</area>
|
||||
<area alias="shortcuts">
|
||||
<key alias="addTab">Добавить вкладку</key>
|
||||
<key alias="addGroup">Добавить вкладку</key>
|
||||
<key alias="addProperty">Добавить свойство</key>
|
||||
<key alias="addEditor">Добавить редактор</key>
|
||||
<key alias="addTemplate">Добавить шаблон</key>
|
||||
|
||||
@@ -490,7 +490,7 @@
|
||||
<key alias="blue">蓝色</key>
|
||||
</area>
|
||||
<area alias="shortcuts">
|
||||
<key alias="addTab">添加选项卡</key>
|
||||
<key alias="addGroup">添加选项卡</key>
|
||||
<key alias="addProperty">添加属性</key>
|
||||
<key alias="addEditor">添加编辑器</key>
|
||||
<key alias="addTemplate">添加模板</key>
|
||||
@@ -980,7 +980,7 @@
|
||||
</area>
|
||||
<area alias="contentTypeEditor">
|
||||
<key alias="compositions">组合</key>
|
||||
<key alias="noTabs">您没有添加任何选项卡</key>
|
||||
<key alias="noGroups">您没有添加任何选项卡</key>
|
||||
<key alias="inheritedFrom">继承自</key>
|
||||
<key alias="addProperty">添加属性</key>
|
||||
<key alias="requiredLabel">必需的标签</key>
|
||||
|
||||
@@ -482,7 +482,7 @@
|
||||
<key alias="blue">藍</key>
|
||||
</area>
|
||||
<area alias="shortcuts">
|
||||
<key alias="addTab">增加標籤頁</key>
|
||||
<key alias="addGroup">增加標籤頁</key>
|
||||
<key alias="addProperty">增加屬性</key>
|
||||
<key alias="addEditor">增加編輯器</key>
|
||||
<key alias="addTemplate">增加範本</key>
|
||||
@@ -967,7 +967,7 @@
|
||||
</area>
|
||||
<area alias="contentTypeEditor">
|
||||
<key alias="compositions">組合</key>
|
||||
<key alias="noTabs">您沒有增加任何選項卡</key>
|
||||
<key alias="noGroups">您沒有增加任何選項卡</key>
|
||||
<key alias="inheritedFrom">繼承的表格</key>
|
||||
<key alias="addProperty">增加屬性</key>
|
||||
<key alias="requiredLabel">必要標籤</key>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Linq;
|
||||
using Umbraco.Core.Cache;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Core.Services.Changes;
|
||||
using Umbraco.Web.PublishedCache;
|
||||
@@ -11,12 +12,14 @@ namespace Umbraco.Web.Cache
|
||||
public sealed class ContentTypeCacheRefresher : PayloadCacheRefresherBase<ContentTypeCacheRefresher, ContentTypeCacheRefresher.JsonPayload>
|
||||
{
|
||||
private readonly IPublishedSnapshotService _publishedSnapshotService;
|
||||
private readonly IPublishedModelFactory _publishedModelFactory;
|
||||
private readonly IdkMap _idkMap;
|
||||
|
||||
public ContentTypeCacheRefresher(AppCaches appCaches, IPublishedSnapshotService publishedSnapshotService, IdkMap idkMap)
|
||||
public ContentTypeCacheRefresher(AppCaches appCaches, IPublishedSnapshotService publishedSnapshotService, IPublishedModelFactory publishedModelFactory, IdkMap idkMap)
|
||||
: base(appCaches)
|
||||
{
|
||||
_publishedSnapshotService = publishedSnapshotService;
|
||||
_publishedModelFactory = publishedModelFactory;
|
||||
_idkMap = idkMap;
|
||||
}
|
||||
|
||||
@@ -75,8 +78,24 @@ namespace Umbraco.Web.Cache
|
||||
// don't try to be clever - refresh all
|
||||
MemberCacheRefresher.RefreshMemberTypes(AppCaches);
|
||||
|
||||
// notify
|
||||
_publishedSnapshotService.Notify(payloads);
|
||||
// we have to refresh models before we notify the published snapshot
|
||||
// service of changes, else factories may try to rebuild models while
|
||||
// we are using the database to load content into caches
|
||||
|
||||
// ReSharper disable once SuspiciousTypeConversion.Global
|
||||
if (_publishedModelFactory is ILivePublishedModelFactory live)
|
||||
{
|
||||
lock (live.SyncRoot)
|
||||
{
|
||||
live.Refresh();
|
||||
_publishedSnapshotService.Notify(payloads);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// ReSharper disable once InconsistentlySynchronizedField
|
||||
_publishedSnapshotService.Notify(payloads);
|
||||
}
|
||||
|
||||
// now we can trigger the event
|
||||
base.Refresh(payloads);
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
using System;
|
||||
using Umbraco.Core.Cache;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
using Umbraco.Core.PropertyEditors.ValueConverters;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Web.PropertyEditors;
|
||||
using Umbraco.Web.PropertyEditors.ValueConverters;
|
||||
using Umbraco.Web.PublishedCache;
|
||||
|
||||
|
||||
@@ -13,12 +12,14 @@ namespace Umbraco.Web.Cache
|
||||
public sealed class DataTypeCacheRefresher : PayloadCacheRefresherBase<DataTypeCacheRefresher, DataTypeCacheRefresher.JsonPayload>
|
||||
{
|
||||
private readonly IPublishedSnapshotService _publishedSnapshotService;
|
||||
private readonly IPublishedModelFactory _publishedModelFactory;
|
||||
private readonly IdkMap _idkMap;
|
||||
|
||||
public DataTypeCacheRefresher(AppCaches appCaches, IPublishedSnapshotService publishedSnapshotService, IdkMap idkMap)
|
||||
public DataTypeCacheRefresher(AppCaches appCaches, IPublishedSnapshotService publishedSnapshotService, IPublishedModelFactory publishedModelFactory, IdkMap idkMap)
|
||||
: base(appCaches)
|
||||
{
|
||||
_publishedSnapshotService = publishedSnapshotService;
|
||||
_publishedModelFactory = publishedModelFactory;
|
||||
_idkMap = idkMap;
|
||||
}
|
||||
|
||||
@@ -60,8 +61,24 @@ namespace Umbraco.Web.Cache
|
||||
TagsValueConverter.ClearCaches();
|
||||
SliderValueConverter.ClearCaches();
|
||||
|
||||
// notify
|
||||
_publishedSnapshotService.Notify(payloads);
|
||||
// we have to refresh models before we notify the published snapshot
|
||||
// service of changes, else factories may try to rebuild models while
|
||||
// we are using the database to load content into caches
|
||||
|
||||
// ReSharper disable once SuspiciousTypeConversion.Global
|
||||
if (_publishedModelFactory is ILivePublishedModelFactory live)
|
||||
{
|
||||
lock (live.SyncRoot)
|
||||
{
|
||||
live.Refresh();
|
||||
_publishedSnapshotService.Notify(payloads);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// ReSharper disable once InconsistentlySynchronizedField
|
||||
_publishedSnapshotService.Notify(payloads);
|
||||
}
|
||||
|
||||
base.Refresh(payloads);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user