diff --git a/.gitignore b/.gitignore
index e03ef6f408..9f22544f35 100644
--- a/.gitignore
+++ b/.gitignore
@@ -119,6 +119,7 @@ build/ApiDocs/*
build/ApiDocs/Output/*
src/Umbraco.Web.UI.Client/bower_components/*
/src/Umbraco.Web.UI/Umbraco/preview
+/src/Umbraco.Web.UI/Umbraco/preview.old
#Ignore Rule for output of generated documentation files from Grunt docserve
src/Umbraco.Web.UI.Client/docs/api
diff --git a/README.md b/README.md
index 045c91fae8..24aa108c11 100644
--- a/README.md
+++ b/README.md
@@ -28,7 +28,7 @@ As an Open Source platform, Umbraco is more than just a CMS. We are transparent
[Umbraco Cloud](https://umbraco.com) is the easiest and fastest way to use Umbraco yet with full support for all your custom .NET code and intergrations. You're up and running in less than a minute and your life will be made easier with automated upgrades and a built-in deployment engine. We offer a free 14 day trial, no credit card needed.
-If you want to DIY you can [download Umbraco](https://our.umbraco.org/download) either as a ZIP file or via NuGet. It's the same version of Umbraco CMS that powers Umbraco Xloud, but you'll need to find a place to host yourself and handling deployments and upgrades is all down to you.
+If you want to DIY you can [download Umbraco](https://our.umbraco.org/download) either as a ZIP file or via NuGet. It's the same version of Umbraco CMS that powers Umbraco Cloud, but you'll need to find a place to host yourself and handling deployments and upgrades is all down to you.
## Community
diff --git a/src/Umbraco.Core/Configuration/UmbracoVersion.cs b/src/Umbraco.Core/Configuration/UmbracoVersion.cs
index a5fe8de270..9c5c38c916 100644
--- a/src/Umbraco.Core/Configuration/UmbracoVersion.cs
+++ b/src/Umbraco.Core/Configuration/UmbracoVersion.cs
@@ -40,8 +40,8 @@ namespace Umbraco.Core.Configuration
Current.Minor,
Current.Build,
CurrentComment.IsNullOrWhiteSpace() ? null : CurrentComment,
- Current.Revision > 0 ? Current.Revision.ToInvariantString() : null);
-
+ Current.Revision > 0 ? Current.Revision.ToInvariantString() : null);
+
///
/// Gets the "local" version of the site.
///
@@ -51,8 +51,8 @@ namespace Umbraco.Core.Configuration
/// and changes during an upgrade. The executing code version changes when new code is
/// deployed. The site/files version changes during an upgrade.
///
- public static SemVersion Local
- {
+ public static SemVersion Local
+ {
get
{
try
@@ -66,6 +66,6 @@ namespace Umbraco.Core.Configuration
return null;
}
}
- }
+ }
}
}
diff --git a/src/Umbraco.Core/Constants-Composing.cs b/src/Umbraco.Core/Constants-Composing.cs
index 34aebddd6a..734a715c2d 100644
--- a/src/Umbraco.Core/Constants-Composing.cs
+++ b/src/Umbraco.Core/Constants-Composing.cs
@@ -22,6 +22,7 @@
public const string MasterpageFileSystem = "MasterpageFileSystem";
public const string ViewFileSystem = "ViewFileSystem";
public const string XsltFileSystem = "XsltFileSystem";
+ public const string JavascriptLibraryFileSystem = "JavascriptLibraryFileSystem";
}
}
}
diff --git a/src/Umbraco.Core/IO/FileSystems.cs b/src/Umbraco.Core/IO/FileSystems.cs
index ab1fbbcf45..91f5721546 100644
--- a/src/Umbraco.Core/IO/FileSystems.cs
+++ b/src/Umbraco.Core/IO/FileSystems.cs
@@ -2,6 +2,7 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Configuration;
+using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
@@ -28,7 +29,7 @@ namespace Umbraco.Core.IO
private ShadowWrapper _xsltFileSystem;
private ShadowWrapper _masterPagesFileSystem;
private ShadowWrapper _mvcViewsFileSystem;
-
+
// well-known file systems lazy initialization
private object _wkfsLock = new object();
private bool _wkfsInitialized;
@@ -36,6 +37,10 @@ namespace Umbraco.Core.IO
private MediaFileSystem _mediaFileSystem;
+
+ //fixme - is this needed to be a managed file system? seems irrelevant since it won't ever be moved and is only used in one place in code
+ private IFileSystem _javascriptLibraryFileSystem;
+
#region Constructor
// DI wants a public ctor
@@ -134,6 +139,16 @@ namespace Umbraco.Core.IO
}
}
+ //fixme - is this needed to be a managed file system? seems irrelevant since it won't ever be moved and is only used in one place in code
+ internal IFileSystem JavascriptLibraryFileSystem
+ {
+ get
+ {
+ if (Volatile.Read(ref _wkfsInitialized) == false) EnsureWellKnownFileSystems();
+ return _javascriptLibraryFileSystem;
+ }
+ }
+
private void EnsureWellKnownFileSystems()
{
LazyInitializer.EnsureInitialized(ref _wkfsObject, ref _wkfsInitialized, ref _wkfsLock, CreateWellKnownFileSystems);
@@ -150,6 +165,7 @@ namespace Umbraco.Core.IO
var xsltFileSystem = new PhysicalFileSystem(SystemDirectories.Xslt);
var masterPagesFileSystem = new PhysicalFileSystem(SystemDirectories.Masterpages);
var mvcViewsFileSystem = new PhysicalFileSystem(SystemDirectories.MvcViews);
+
_macroPartialFileSystem = new ShadowWrapper(macroPartialFileSystem, "Views/MacroPartials", () => IsScoped());
_partialViewsFileSystem = new ShadowWrapper(partialViewsFileSystem, "Views/Partials", () => IsScoped());
@@ -159,6 +175,8 @@ namespace Umbraco.Core.IO
_masterPagesFileSystem = new ShadowWrapper(masterPagesFileSystem, "masterpages", () => IsScoped());
_mvcViewsFileSystem = new ShadowWrapper(mvcViewsFileSystem, "Views", () => IsScoped());
+ _javascriptLibraryFileSystem = new PhysicalFileSystem(Path.Combine(SystemDirectories.Umbraco, "lib"));
+
// filesystems obtained from GetFileSystemProvider are already wrapped and do not need to be wrapped again
_mediaFileSystem = GetFileSystemProvider();
diff --git a/src/Umbraco.Core/IO/SystemDirectories.cs b/src/Umbraco.Core/IO/SystemDirectories.cs
index ee77ddeb54..f2c9a438db 100644
--- a/src/Umbraco.Core/IO/SystemDirectories.cs
+++ b/src/Umbraco.Core/IO/SystemDirectories.cs
@@ -181,7 +181,6 @@ namespace Umbraco.Core.IO
{
get
{
- //by default the packages folder should exist in the data folder
return IOHelper.ReturnPath("umbracoPreviewPath", Data + IOHelper.DirSepChar + "preview");
}
}
diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseBuilder.cs b/src/Umbraco.Core/Migrations/Install/DatabaseBuilder.cs
index b39bca7eae..452f9dc688 100644
--- a/src/Umbraco.Core/Migrations/Install/DatabaseBuilder.cs
+++ b/src/Umbraco.Core/Migrations/Install/DatabaseBuilder.cs
@@ -302,6 +302,22 @@ namespace Umbraco.Core.Migrations.Install
var connectionStrings = xml.Root.DescendantsAndSelf("connectionStrings").FirstOrDefault();
if (connectionStrings == null) throw new Exception("Invalid web.config file.");
+ // honour configSource, if its set, change the xml file we are saving the configuration
+ // to the one set in the configSource attribute
+ if (connectionStrings.Attribute("configSource") != null)
+ {
+ var source = connectionStrings.Attribute("configSource").Value;
+ var configFile = IOHelper.MapPath($"{SystemDirectories.Root}/{source}");
+ logger.Info("storing ConnectionString in {0}", () => configFile);
+ if (File.Exists(configFile))
+ {
+ xml = XDocument.Load(fileName, LoadOptions.PreserveWhitespace);
+ fileName = configFile;
+ }
+ connectionStrings = xml.Root.DescendantsAndSelf("connectionStrings").FirstOrDefault();
+ if (connectionStrings == null) throw new Exception("Invalid web.config file.");
+ }
+
// update connectionString if it exists, or else create a new connectionString
var setting = connectionStrings.Descendants("add").FirstOrDefault(s => s.Attribute("name").Value == Constants.System.UmbracoConnectionName);
if (setting == null)
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_10_0/RenamePreviewFolder.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_10_0/RenamePreviewFolder.cs
new file mode 100644
index 0000000000..4f404b295a
--- /dev/null
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_7_10_0/RenamePreviewFolder.cs
@@ -0,0 +1,39 @@
+using System.IO;
+using Umbraco.Core.IO;
+using Umbraco.Core.Logging;
+using Umbraco.Core.Persistence.SqlSyntax;
+using File = System.IO.File;
+
+namespace Umbraco.Core.Migrations.Upgrade.V_7_10_0
+{
+ ///
+ /// Renames the preview folder containing static html files to ensure it does not interfere with the MVC route
+ /// that is now supposed to render these views dynamically. We don't want to delete as people may have made
+ /// customizations to these files that would need to be migrated to the new .cshtml view files.
+ ///
+ public class RenamePreviewFolder : MigrationBase
+ {
+ public RenamePreviewFolder(IMigrationContext context) : base(context)
+ {
+ }
+
+ public override void Migrate()
+ {
+ var previewFolderPath = IOHelper.MapPath(SystemDirectories.Umbraco + "/preview");
+ if (Directory.Exists(previewFolderPath))
+ {
+ var newPath = previewFolderPath.Replace("preview", "preview.old");
+ if (Directory.Exists(newPath) == false)
+ {
+ Directory.Move(previewFolderPath, newPath);
+ var readmeText =
+ $"Static html files used for preview and canvas editing functionality no longer live in this directory.\r\n" +
+ $"Instead they have been recreated as MVC views and can now be found in '~/Umbraco/Views/Preview'.\r\n" +
+ $"See issue: http://issues.umbraco.org/issue/U4-11090";
+ File.WriteAllText(Path.Combine(newPath, "readme.txt"), readmeText);
+ }
+ }
+ }
+
+ }
+}
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_7_0/AddUserGroupTables.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_7_0/AddUserGroupTables.cs
index 769a953062..1042322dca 100644
--- a/src/Umbraco.Core/Migrations/Upgrade/V_7_7_0/AddUserGroupTables.cs
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_7_7_0/AddUserGroupTables.cs
@@ -2,8 +2,11 @@
using System.Collections.Generic;
using System.Data;
using System.Linq;
+using NPoco;
+using Umbraco.Core.Persistence;
using Umbraco.Core.Persistence.Dtos;
using Umbraco.Core.Persistence.SqlSyntax;
+using ColumnInfo = Umbraco.Core.Persistence.SqlSyntax.ColumnInfo;
namespace Umbraco.Core.Migrations.Upgrade.V_7_7_0
{
@@ -339,14 +342,24 @@ namespace Umbraco.Core.Migrations.Upgrade.V_7_7_0
if (tables.InvariantContains("umbracoUserType") && tables.InvariantContains("umbracoUser"))
{
- if (constraints.Any(x => x.Item1.InvariantEquals("umbracoUser") && x.Item3.InvariantEquals("FK_umbracoUser_umbracoUserType_id")))
+ if (DatabaseType.IsMySql())
{
- Delete.ForeignKey("FK_umbracoUser_umbracoUserType_id").OnTable("umbracoUser").Do();
+ //In MySql, this will drop the FK according to it's special naming rules
+ Delete.ForeignKey().FromTable("umbracoUser").ForeignColumn("userType").ToTable("umbracoUserType").PrimaryColumn("id");
}
- //This is the super old constraint name of the FK for user type so check this one too
- if (constraints.Any(x => x.Item1.InvariantEquals("umbracoUser") && x.Item3.InvariantEquals("FK_user_userType")))
+ else
{
- Delete.ForeignKey("FK_user_userType").OnTable("umbracoUser").Do();
+ //Delete the FK if it exists before dropping the column
+ if (constraints.Any(x => x.Item1.InvariantEquals("umbracoUser") && x.Item3.InvariantEquals("FK_umbracoUser_umbracoUserType_id")))
+ {
+ Delete.ForeignKey("FK_umbracoUser_umbracoUserType_id").OnTable("umbracoUser").Do();
+ }
+
+ //This is the super old constraint name of the FK for user type so check this one too
+ if (constraints.Any(x => x.Item1.InvariantEquals("umbracoUser") && x.Item3.InvariantEquals("FK_user_userType")))
+ {
+ Delete.ForeignKey("FK_user_userType").OnTable("umbracoUser").Do();
+ }
}
Delete.Column("userType").FromTable("umbracoUser").Do();
diff --git a/src/Umbraco.Core/ObjectExtensions.cs b/src/Umbraco.Core/ObjectExtensions.cs
index 4f48322a8f..fb994f92f5 100644
--- a/src/Umbraco.Core/ObjectExtensions.cs
+++ b/src/Umbraco.Core/ObjectExtensions.cs
@@ -47,7 +47,7 @@ namespace Umbraco.Core
///
public static void DisposeIfDisposable(this object input)
{
- if (input is IDisposable disposable)
+ if (input is IDisposable disposable)
disposable.Dispose();
}
diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/IntegerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/IntegerValueConverter.cs
index c0933d8605..c0fa4bb88c 100644
--- a/src/Umbraco.Core/PropertyEditors/ValueConverters/IntegerValueConverter.cs
+++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/IntegerValueConverter.cs
@@ -17,24 +17,12 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters
public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview)
{
- if (source == null) return 0;
-
- // in XML an integer is a string
- var sourceString = source as string;
- if (sourceString != null)
- {
- int i;
- return (int.TryParse(sourceString, out i)) ? i : 0;
- }
+ return source.TryConvertTo().Result;
// in json an integer comes back as Int64
// ignore overflows ;(
if (source is long)
return Convert.ToInt32(source);
-
- // in the database an integer is an integer
- // default value is zero
- return (source is int) ? source : 0;
}
}
}
diff --git a/src/Umbraco.Core/Services/IdkMap.cs b/src/Umbraco.Core/Services/IdkMap.cs
index b320bd16d7..51c5abd900 100644
--- a/src/Umbraco.Core/Services/IdkMap.cs
+++ b/src/Umbraco.Core/Services/IdkMap.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using Umbraco.Core.Models;
@@ -23,12 +24,139 @@ namespace Umbraco.Core.Services
// note - for pure read-only we might want to *not* enforce a transaction?
+ // notes
+ //
+ // - this class assumes that the id/guid map is unique; that is, if an id and a guid map
+ // to each other, then the id will never map to another guid, and the guid will never map
+ // to another id
+ //
+ // - LeeK's solution in 7.7 was to look for the id/guid in the content cache "on demand" via
+ // XPath, which is probably fast enough but cannot deal with media ids + it maintains a
+ // separate, duplicate cache
+ // see https://github.com/umbraco/Umbraco-CMS/pull/2398
+ //
+ // - Andy's solution in a package was to prefetch all by sql; it cannot prefecth reserved ids
+ // as we don't know the corresponding object type, but that's not a big issue - but then we
+ // have a full database query on startup
+ // see https://github.com/AndyButland/UmbracoUdiToIdCache
+ //
+ // - the original IdkMap implementation that was used by services, did a database lookup on
+ // each cache miss, which is fine enough for services, but would be really slow at content
+ // cache level
+ //
+ // - cache is cleared by MediaCacheRefresher, UnpublishedPageCacheRefresher, and other
+ // refreshers - because id/guid map is unique, we only clear to avoid leaking memory, 'cos
+ // we don't risk caching obsolete values - and only when actually deleting
+ //
+ // so...
+ //
+ // - there's a single caching point, and it's idkMap
+ // - there are no "helper methods" - the published content cache itself knows about Guids
+ // - when the published content cache is instanciated, it populates the idkMap with what it knows
+ // and it registers a way for the idkMap to look for id/keys in the published content cache
+ // - we do NOT prefetch anything from database
+ // - when a request comes in:
+ // the published content cache uses the idkMap to map id/key
+ // if the idkMap already knows about the map, it returns the value
+ // else it tries the published cache via XPath
+ // else it hits the database
+
+
+ private readonly ConcurrentDictionary id2key, Func key2id)> _dictionary
+ = new ConcurrentDictionary id2key, Func key2id)>();
+
+ internal void SetMapper(UmbracoObjectTypes umbracoObjectType, Func id2key, Func key2id)
+ {
+ _dictionary[umbracoObjectType] = (id2key, key2id);
+ }
+
+ internal void Populate(IEnumerable<(int id, Guid key)> pairs, UmbracoObjectTypes umbracoObjectType)
+ {
+ try
+ {
+ _locker.EnterWriteLock();
+ foreach (var pair in pairs)
+ {
+ _id2Key.Add(pair.id, new TypedId(pair.key, umbracoObjectType));
+ _key2Id.Add(pair.key, new TypedId(pair.id, umbracoObjectType));
+ }
+ }
+ finally
+ {
+ if (_locker.IsWriteLockHeld)
+ _locker.ExitWriteLock();
+ }
+ }
+
+#if POPULATE_FROM_DATABASE
+ private void PopulateLocked()
+ {
+ // don't if not empty
+ if (_key2Id.Count > 0) return;
+
+ using (var uow = _uowProvider.GetUnitOfWork())
+ {
+ // populate content and media items
+ var types = new[] { Constants.ObjectTypes.Document, Constants.ObjectTypes.Media };
+ var values = uow.Database.Query("SELECT id, uniqueId, nodeObjectType FROM umbracoNode WHERE nodeObjectType IN @types", new { types });
+ foreach (var value in values)
+ {
+ var umbracoObjectType = UmbracoObjectTypesExtensions.GetUmbracoObjectType(value.NodeObjectType);
+ _id2Key.Add(value.Id, new TypedId(value.UniqueId, umbracoObjectType));
+ _key2Id.Add(value.UniqueId, new TypedId(value.Id, umbracoObjectType));
+ }
+ }
+ }
+
+ private Attempt PopulateAndGetIdForKey(Guid key, UmbracoObjectTypes umbracoObjectType)
+ {
+ try
+ {
+ _locker.EnterWriteLock();
+
+ PopulateLocked();
+
+ return _key2Id.TryGetValue(key, out var id) && id.UmbracoObjectType == umbracoObjectType
+ ? Attempt.Succeed(id.Id)
+ : Attempt.Fail();
+
+ }
+ finally
+ {
+ if (_locker.IsReadLockHeld)
+ _locker.ExitReadLock();
+ }
+ }
+
+ private Attempt PopulateAndGetKeyForId(int id, UmbracoObjectTypes umbracoObjectType)
+ {
+ try
+ {
+ _locker.EnterWriteLock();
+
+ PopulateLocked();
+
+ return _id2Key.TryGetValue(id, out var key) && key.UmbracoObjectType == umbracoObjectType
+ ? Attempt.Succeed(key.Id)
+ : Attempt.Fail();
+ }
+ finally
+ {
+ if (_locker.IsReadLockHeld)
+ _locker.ExitReadLock();
+ }
+ }
+#endif
+
public Attempt GetIdForKey(Guid key, UmbracoObjectTypes umbracoObjectType)
{
+ bool empty;
+
try
{
_locker.EnterReadLock();
if (_key2Id.TryGetValue(key, out var id) && id.UmbracoObjectType == umbracoObjectType) return Attempt.Succeed(id.Id);
+ empty = _key2Id.Count == 0;
}
finally
{
@@ -36,20 +164,37 @@ namespace Umbraco.Core.Services
_locker.ExitReadLock();
}
- int? val;
- using (var scope = _scopeProvider.CreateScope())
- {
- var sql = scope.Database.SqlContext.Sql()
- .Select(x => x.NodeId).From().Where(x => x.UniqueId == key);
+#if POPULATE_FROM_DATABASE
+ // if cache is empty and looking for a document or a media,
+ // populate the cache at once and return what we found
+ if (empty && (umbracoObjectType == UmbracoObjectTypes.Document || umbracoObjectType == UmbracoObjectTypes.Media))
+ return PopulateAndGetIdForKey(key, umbracoObjectType);
+#endif
- if (umbracoObjectType != UmbracoObjectTypes.Unknown) // if unknow, don't include in query
+ // optimize for read speed: reading database outside a lock means that we could read
+ // multiple times, but we don't lock the cache while accessing the database = better
+
+ int? val = null;
+
+ if (_dictionary.TryGetValue(umbracoObjectType, out var mappers))
+ if ((val = mappers.key2id(key)) == default(int)) val = null;
+
+ if (val == null)
+ {
+ using (var uow = _uowProvider.GetUnitOfWork())
{
- var objectType = GetNodeObjectTypeGuid(umbracoObjectType);
- sql = sql.Where(x => x.NodeObjectType == objectType || x.NodeObjectType == Constants.ObjectTypes.IdReservation); // fixme TEST the OR here!
+ //if it's unknown don't include the nodeObjectType in the query
+ if (umbracoObjectType == UmbracoObjectTypes.Unknown)
+ {
+ val = uow.Database.ExecuteScalar("SELECT id FROM umbracoNode WHERE uniqueId=@id", new { id = key});
+ }
+ else
+ {
+ val = uow.Database.ExecuteScalar("SELECT id FROM umbracoNode WHERE uniqueId=@id AND (nodeObjectType=@type OR nodeObjectType=@reservation)",
+ new { id = key, type = GetNodeObjectTypeGuid(umbracoObjectType), reservation = Constants.ObjectTypes.IdReservationGuid });
+ }
+ uow.Commit();
}
-
- val = scope.Database.ExecuteScalar(sql);
- scope.Complete();
}
if (val == null) return Attempt.Fail();
@@ -83,12 +228,23 @@ namespace Umbraco.Core.Services
return GetIdForKey(guidUdi.Guid, umbracoType);
}
+ public Attempt GetUdiForId(int id, UmbracoObjectTypes umbracoObjectType)
+ {
+ var keyAttempt = GetKeyForId(id, umbracoObjectType);
+ return keyAttempt
+ ? Attempt.Succeed(new GuidUdi(Constants.UdiEntityType.FromUmbracoObjectType(umbracoObjectType), keyAttempt.Result))
+ : Attempt.Fail();
+ }
+
public Attempt GetKeyForId(int id, UmbracoObjectTypes umbracoObjectType)
{
+ bool empty;
+
try
{
_locker.EnterReadLock();
if (_id2Key.TryGetValue(id, out var key) && key.UmbracoObjectType == umbracoObjectType) return Attempt.Succeed(key.Id);
+ empty = _id2Key.Count == 0;
}
finally
{
@@ -96,20 +252,37 @@ namespace Umbraco.Core.Services
_locker.ExitReadLock();
}
- Guid? val;
- using (var scope = _scopeProvider.CreateScope())
- {
- var sql = scope.Database.SqlContext.Sql()
- .Select(x => x.UniqueId).From().Where(x => x.NodeId == id);
+#if POPULATE_FROM_DATABASE
+ // if cache is empty and looking for a document or a media,
+ // populate the cache at once and return what we found
+ if (empty && (umbracoObjectType == UmbracoObjectTypes.Document || umbracoObjectType == UmbracoObjectTypes.Media))
+ return PopulateAndGetKeyForId(id, umbracoObjectType);
+#endif
- if (umbracoObjectType != UmbracoObjectTypes.Unknown) // if unknow, don't include in query
+ // optimize for read speed: reading database outside a lock means that we could read
+ // multiple times, but we don't lock the cache while accessing the database = better
+
+ Guid? val = null;
+
+ if (_dictionary.TryGetValue(umbracoObjectType, out var mappers))
+ if ((val = mappers.id2key(id)) == default(Guid)) val = null;
+
+ if (val == null)
+ {
+ using (var uow = _uowProvider.GetUnitOfWork())
{
- var objectType = GetNodeObjectTypeGuid(umbracoObjectType);
- sql = sql.Where(x => x.NodeObjectType == objectType || x.NodeObjectType == Constants.ObjectTypes.IdReservation); // fixme TEST the OR here!
+ //if it's unknown don't include the nodeObjectType in the query
+ if (umbracoObjectType == UmbracoObjectTypes.Unknown)
+ {
+ val = uow.Database.ExecuteScalar("SELECT uniqueId FROM umbracoNode WHERE id=@id", new { id });
+ }
+ else
+ {
+ val = uow.Database.ExecuteScalar("SELECT uniqueId FROM umbracoNode WHERE id=@id AND (nodeObjectType=@type OR nodeObjectType=@reservation)",
+ new { id, type = GetNodeObjectTypeGuid(umbracoObjectType), reservation = Constants.ObjectTypes.IdReservationGuid });
+ }
+ uow.Commit();
}
-
- val = scope.Database.ExecuteScalar(sql);
- scope.Complete();
}
if (val == null) return Attempt.Fail();
@@ -141,6 +314,8 @@ namespace Umbraco.Core.Services
return guid;
}
+ // invoked on UnpublishedPageCacheRefresher.RefreshAll
+ // anything else will use the id-specific overloads
public void ClearCache()
{
try
@@ -188,17 +363,28 @@ namespace Umbraco.Core.Services
}
}
+ // ReSharper disable ClassNeverInstantiated.Local
+ // ReSharper disable UnusedAutoPropertyAccessor.Local
+ private class TypedIdDto
+ {
+ public int Id { get; set; }
+ public Guid UniqueId { get; set; }
+ public Guid NodeObjectType { get; set; }
+ }
+ // ReSharper restore ClassNeverInstantiated.Local
+ // ReSharper restore UnusedAutoPropertyAccessor.Local
+
private struct TypedId
{
- public T Id { get; }
-
- public UmbracoObjectTypes UmbracoObjectType { get; }
-
public TypedId(T id, UmbracoObjectTypes umbracoObjectType)
{
UmbracoObjectType = umbracoObjectType;
Id = id;
}
+
+ public UmbracoObjectTypes UmbracoObjectType { get; }
+
+ public T Id { get; }
}
}
}
diff --git a/src/Umbraco.Core/Services/Implement/UserService.cs b/src/Umbraco.Core/Services/Implement/UserService.cs
index 43d438da0b..1c220157a9 100644
--- a/src/Umbraco.Core/Services/Implement/UserService.cs
+++ b/src/Umbraco.Core/Services/Implement/UserService.cs
@@ -196,28 +196,26 @@ namespace Umbraco.Core.Services.Implement
{
return _userRepository.GetByUsername(username, includeSecurityData: true);
}
- catch (Exception ex)
+ catch (DbException ex)
{
- if (ex is SqlException || ex is SqlCeException)
+ // fixme kill in v8
+ //we need to handle this one specific case which is when we are upgrading to 7.7 since the user group
+ //tables don't exist yet. This is the 'easiest' way to deal with this without having to create special
+ //version checks in the BackOfficeSignInManager and calling into other special overloads that we'd need
+ //like "GetUserById(int id, bool includeSecurityData)" which may cause confusion because the result of
+ //that method would not be cached.
+ // fixme kill in v8
+ //we need to handle this one specific case which is when we are upgrading to 7.7 since the user group
+ //tables don't exist yet. This is the 'easiest' way to deal with this without having to create special
+ //version checks in the BackOfficeSignInManager and calling into other special overloads that we'd need
+ //like "GetUserById(int id, bool includeSecurityData)" which may cause confusion because the result of
+ //that method would not be cached.
+ if (_isUpgrading)
{
- // fixme kill in v8
- //we need to handle this one specific case which is when we are upgrading to 7.7 since the user group
- //tables don't exist yet. This is the 'easiest' way to deal with this without having to create special
- //version checks in the BackOfficeSignInManager and calling into other special overloads that we'd need
- //like "GetUserById(int id, bool includeSecurityData)" which may cause confusion because the result of
- //that method would not be cached.
- // fixme kill in v8
- //we need to handle this one specific case which is when we are upgrading to 7.7 since the user group
- //tables don't exist yet. This is the 'easiest' way to deal with this without having to create special
- //version checks in the BackOfficeSignInManager and calling into other special overloads that we'd need
- //like "GetUserById(int id, bool includeSecurityData)" which may cause confusion because the result of
- //that method would not be cached.
- if (_isUpgrading)
- {
- //NOTE: this will not be cached
- return _userRepository.GetByUsername(username, includeSecurityData: false);
- }
+ //NOTE: this will not be cached
+ return _userRepository.GetByUsername(username, includeSecurityData: false);
}
+
throw;
}
}
@@ -735,22 +733,20 @@ namespace Umbraco.Core.Services.Implement
{
return _userRepository.Get(id);
}
- catch (Exception ex)
+ catch (DbException ex)
{
- if (ex is SqlException || ex is SqlCeException)
+ // fixme kill in v8
+ //we need to handle this one specific case which is when we are upgrading to 7.7 since the user group
+ //tables don't exist yet. This is the 'easiest' way to deal with this without having to create special
+ //version checks in the BackOfficeSignInManager and calling into other special overloads that we'd need
+ //like "GetUserById(int id, bool includeSecurityData)" which may cause confusion because the result of
+ //that method would not be cached.
+ if (_isUpgrading)
{
- // fixme kill in v8
- //we need to handle this one specific case which is when we are upgrading to 7.7 since the user group
- //tables don't exist yet. This is the 'easiest' way to deal with this without having to create special
- //version checks in the BackOfficeSignInManager and calling into other special overloads that we'd need
- //like "GetUserById(int id, bool includeSecurityData)" which may cause confusion because the result of
- //that method would not be cached.
- if (_isUpgrading)
- {
- //NOTE: this will not be cached
- return _userRepository.Get(id, includeSecurityData: false);
- }
+ //NOTE: this will not be cached
+ return _userRepository.Get(id, includeSecurityData: false);
}
+
throw;
}
}
diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj
index 2d8c7f2561..2fa8cc3887 100644
--- a/src/Umbraco.Core/Umbraco.Core.csproj
+++ b/src/Umbraco.Core/Umbraco.Core.csproj
@@ -45,6 +45,9 @@
+
+ ..\packages\System.ValueTuple.4.4.0\lib\netstandard1.0\System.ValueTuple.dll
+
@@ -581,6 +584,103 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj b/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj
index 70474cdff0..8021c71ba8 100644
--- a/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj
+++ b/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj
@@ -151,6 +151,7 @@
..\packages\System.Security.Cryptography.X509Certificates.4.3.0\lib\net46\System.Security.Cryptography.X509Certificates.dllTrue
+ ..\packages\System.ValueTuple.4.4.0\lib\netstandard1.0\System.ValueTuple.dll
diff --git a/src/Umbraco.Tests.Benchmarks/app.config b/src/Umbraco.Tests.Benchmarks/app.config
new file mode 100644
index 0000000000..8af971b1f8
--- /dev/null
+++ b/src/Umbraco.Tests.Benchmarks/app.config
@@ -0,0 +1,130 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Umbraco.Tests.Benchmarks/packages.config b/src/Umbraco.Tests.Benchmarks/packages.config
index f8d7dec738..438fb232b4 100644
--- a/src/Umbraco.Tests.Benchmarks/packages.config
+++ b/src/Umbraco.Tests.Benchmarks/packages.config
@@ -53,7 +53,7 @@
-
+
diff --git a/src/Umbraco.Tests/UI/LegacyDialogTests.cs b/src/Umbraco.Tests/UI/LegacyDialogTests.cs
index 3fe64d6e03..50d9a5bbb6 100644
--- a/src/Umbraco.Tests/UI/LegacyDialogTests.cs
+++ b/src/Umbraco.Tests/UI/LegacyDialogTests.cs
@@ -4,6 +4,7 @@ using Umbraco.Core;
using umbraco;
using Umbraco.Core.Composing;
using Umbraco.Web._Legacy.UI;
+using Umbraco.Web.umbraco.presentation.umbraco.create;
namespace Umbraco.Tests.UI
{
diff --git a/src/Umbraco.Web.UI.Client/bower.json b/src/Umbraco.Web.UI.Client/bower.json
index 2aaa4fc1d4..f63e987755 100644
--- a/src/Umbraco.Web.UI.Client/bower.json
+++ b/src/Umbraco.Web.UI.Client/bower.json
@@ -43,7 +43,12 @@
"codemirror"
],
"sources": {
- "moment": "bower_components/moment/min/moment-with-locales.js",
+ "moment": [
+ "bower_components/moment/min/moment.min.js",
+ "bower_components/moment/min/moment-with-locales.js",
+ "bower_components/moment/min/moment-with-locales.min.js",
+ "bower_components/moment/locale/*.js"
+ ],
"underscore": [
"bower_components/underscore/underscore-min.js",
"bower_components/underscore/underscore-min.map"
diff --git a/src/Umbraco.Web.UI.Client/gulpfile.js b/src/Umbraco.Web.UI.Client/gulpfile.js
index 841c0476e2..bb3ff65cf0 100644
--- a/src/Umbraco.Web.UI.Client/gulpfile.js
+++ b/src/Umbraco.Web.UI.Client/gulpfile.js
@@ -73,7 +73,6 @@ var sources = {
js: {
preview: { files: ["src/canvasdesigner/**/*.js"], out: "umbraco.canvasdesigner.js" },
installer: { files: ["src/installer/**/*.js"], out: "umbraco.installer.js" },
-
controllers: { files: ["src/{views,controllers}/**/*.controller.js"], out: "umbraco.controllers.js" },
directives: { files: ["src/common/directives/**/*.js"], out: "umbraco.directives.js" },
filters: { files: ["src/common/filters/**/*.js"], out: "umbraco.filters.js" },
@@ -85,8 +84,7 @@ var sources = {
//selectors for copying all views into the build
//processed in the views task
views:{
- umbraco: {files: ["src/views/**/*html"], folder: ""},
- preview: { files: ["src/canvasdesigner/**/*.html"], folder: "../preview"},
+ umbraco: {files: ["src/views/**/*.html"], folder: ""},
installer: {files: ["src/installer/steps/*.html"], folder: "install"}
},
diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json
index b1ee7be7bf..a25176927c 100644
--- a/src/Umbraco.Web.UI.Client/package.json
+++ b/src/Umbraco.Web.UI.Client/package.json
@@ -27,9 +27,9 @@
"gulp": "^3.9.1",
"gulp-concat": "^2.6.0",
"gulp-connect": "5.0.0",
- "gulp-less": "^3.1.0",
+ "gulp-less": "^3.5.0",
"gulp-ngdocs": "^0.3.0",
- "gulp-open": "^2.0.0",
+ "gulp-open": "^2.1.0",
"gulp-postcss": "^6.2.0",
"gulp-rename": "^1.2.2",
"gulp-sort": "^2.0.0",
@@ -38,11 +38,11 @@
"gulp-wrap-js": "^0.4.1",
"jasmine-core": "2.5.2",
"karma": "^1.7.0",
- "karma-jasmine": "^1.1.0",
+ "karma-jasmine": "^1.1.1",
"karma-phantomjs-launcher": "^1.0.4",
- "less": "^2.6.1",
- "lodash": "^4.16.3",
+ "less": "^2.7.3",
+ "lodash": "^4.17.5",
"merge-stream": "^1.0.1",
- "run-sequence": "^2.1.0"
+ "run-sequence": "^2.2.1"
}
}
diff --git a/src/Umbraco.Web.UI.Client/src/canvasdesigner/editors/color.html b/src/Umbraco.Web.UI.Client/src/canvasdesigner/editors/color.html
deleted file mode 100644
index 94a412f128..0000000000
--- a/src/Umbraco.Web.UI.Client/src/canvasdesigner/editors/color.html
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js
index 1ecb2d7403..4452e35835 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js
@@ -255,11 +255,9 @@
else {
$scope.save().then(function (data) {
previewWindow.location.href = redirect;
- });
+ });
}
-
}
-
};
$scope.restore = function (content) {
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js
index 2ce0c5a22e..b2bbd9006a 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js
@@ -8,6 +8,8 @@
var evts = [];
var isInfoTab = false;
scope.publishStatus = {};
+
+ scope.disableTemplates = Umbraco.Sys.ServerVariables.features.disabledFeatures.disableTemplates;
function onInit() {
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js
index 90daac73db..e18137085b 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js
@@ -145,6 +145,12 @@ angular.module("umbraco.directives")
}
}
}
+ if (val === "true") {
+ tinyMceConfig.customConfig[i] = true;
+ }
+ if (val === "false") {
+ tinyMceConfig.customConfig[i] = false;
+ }
}
angular.extend(baseLineConfigObj, tinyMceConfig.customConfig);
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js
index 6e705da52d..5a757f3a29 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js
@@ -495,6 +495,7 @@ Opens an overlay to show a custom YSOD.
var activeElementType = document.activeElement.tagName;
var clickableElements = ["A", "BUTTON"];
var submitOnEnter = document.activeElement.hasAttribute("overlay-submit-on-enter");
+ var submitOnEnterValue = submitOnEnter ? document.activeElement.getAttribute("overlay-submit-on-enter") : "";
if(clickableElements.indexOf(activeElementType) === 0) {
document.activeElement.click();
@@ -502,7 +503,9 @@ Opens an overlay to show a custom YSOD.
} else if(activeElementType === "TEXTAREA" && !submitOnEnter) {
- } else {
+ } else if (submitOnEnter && submitOnEnterValue === "false") {
+ // don't do anything
+ }else {
scope.$apply(function () {
scope.submitForm(scope.model);
});
diff --git a/src/Umbraco.Web.UI.Client/src/common/services/javascriptlibrary.service.js b/src/Umbraco.Web.UI.Client/src/common/services/javascriptlibrary.service.js
new file mode 100644
index 0000000000..4829dff506
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/common/services/javascriptlibrary.service.js
@@ -0,0 +1,37 @@
+(function () {
+ "use strict";
+
+ function javascriptLibraryService($q, $http, umbRequestHelper) {
+
+ var existingLocales = [];
+
+ function getSupportedLocalesForMoment() {
+ var deferred = $q.defer();
+
+ if (existingLocales.length === 0) {
+ umbRequestHelper.resourcePromise(
+ $http.get(
+ umbRequestHelper.getApiUrl(
+ "backOfficeAssetsApiBaseUrl",
+ "GetSupportedMomentLocales")),
+ "Failed to get cultures").then(function(locales) {
+ existingLocales = locales;
+ deferred.resolve(existingLocales);
+ });
+ } else {
+ deferred.resolve(existingLocales);
+ }
+
+ return deferred.promise;
+ }
+
+ var service = {
+ getSupportedLocalesForMoment: getSupportedLocalesForMoment
+ };
+
+ return service;
+ }
+
+ angular.module("umbraco.services").factory("javascriptLibraryService", javascriptLibraryService);
+
+})();
diff --git a/src/Umbraco.Web.UI.Client/src/common/services/user.service.js b/src/Umbraco.Web.UI.Client/src/common/services/user.service.js
index f20c3df44f..eda46fbb71 100644
--- a/src/Umbraco.Web.UI.Client/src/common/services/user.service.js
+++ b/src/Umbraco.Web.UI.Client/src/common/services/user.service.js
@@ -1,285 +1,324 @@
angular.module('umbraco.services')
- .factory('userService', function ($rootScope, eventsService, $q, $location, $log, securityRetryQueue, authResource, dialogService, $timeout, angularHelper, $http) {
+ .factory('userService', function ($rootScope, eventsService, $q, $location, $log, securityRetryQueue, authResource, assetsService, dialogService, $timeout, angularHelper, $http, javascriptLibraryService) {
- var currentUser = null;
- var lastUserId = null;
- var loginDialog = null;
+ var currentUser = null;
+ var lastUserId = null;
+ var loginDialog = null;
- //this tracks the last date/time that the user's remainingAuthSeconds was updated from the server
- // this is used so that we know when to go and get the user's remaining seconds directly.
- var lastServerTimeoutSet = null;
+ //this tracks the last date/time that the user's remainingAuthSeconds was updated from the server
+ // this is used so that we know when to go and get the user's remaining seconds directly.
+ var lastServerTimeoutSet = null;
- function openLoginDialog(isTimedOut) {
- if (!loginDialog) {
- loginDialog = dialogService.open({
+ function openLoginDialog(isTimedOut) {
+ if (!loginDialog) {
+ loginDialog = dialogService.open({
- //very special flag which means that global events cannot close this dialog
- manualClose: true,
+ //very special flag which means that global events cannot close this dialog
+ manualClose: true,
- template: 'views/common/dialogs/login.html',
- modalClass: "login-overlay",
- animation: "slide",
- show: true,
- callback: onLoginDialogClose,
- dialogData: {
- isTimedOut: isTimedOut
- }
- });
- }
- }
-
- function onLoginDialogClose(success) {
- loginDialog = null;
-
- if (success) {
- securityRetryQueue.retryAll(currentUser.name);
- }
- else {
- securityRetryQueue.cancelAll();
- $location.path('/');
- }
- }
-
- /**
- This methods will set the current user when it is resolved and
- will then start the counter to count in-memory how many seconds they have
- remaining on the auth session
- */
- function setCurrentUser(usr) {
- if (!usr.remainingAuthSeconds) {
- throw "The user object is invalid, the remainingAuthSeconds is required.";
- }
- currentUser = usr;
- lastServerTimeoutSet = new Date();
- //start the timer
- countdownUserTimeout();
- }
-
- /**
- Method to count down the current user's timeout seconds,
- this will continually count down their current remaining seconds every 5 seconds until
- there are no more seconds remaining.
- */
- function countdownUserTimeout() {
-
- $timeout(function () {
-
- if (currentUser) {
- //countdown by 5 seconds since that is how long our timer is for.
- currentUser.remainingAuthSeconds -= 5;
-
- //if there are more than 30 remaining seconds, recurse!
- if (currentUser.remainingAuthSeconds > 30) {
-
- //we need to check when the last time the timeout was set from the server, if
- // it has been more than 30 seconds then we'll manually go and retrieve it from the
- // server - this helps to keep our local countdown in check with the true timeout.
- if (lastServerTimeoutSet != null) {
- var now = new Date();
- var seconds = (now.getTime() - lastServerTimeoutSet.getTime()) / 1000;
-
- if (seconds > 30) {
-
- //first we'll set the lastServerTimeoutSet to null - this is so we don't get back in to this loop while we
- // wait for a response from the server otherwise we'll be making double/triple/etc... calls while we wait.
- lastServerTimeoutSet = null;
-
- //now go get it from the server
- //NOTE: the safeApply because our timeout is set to not run digests (performance reasons)
- angularHelper.safeApply($rootScope, function () {
- authResource.getRemainingTimeoutSeconds().then(function (result) {
- setUserTimeoutInternal(result);
- });
+ template: 'views/common/dialogs/login.html',
+ modalClass: "login-overlay",
+ animation: "slide",
+ show: true,
+ callback: onLoginDialogClose,
+ dialogData: {
+ isTimedOut: isTimedOut
+ }
});
- }
}
+ }
- //recurse the countdown!
- countdownUserTimeout();
- }
- else {
+ function onLoginDialogClose(success) {
+ loginDialog = null;
- //we are either timed out or very close to timing out so we need to show the login dialog.
- if (Umbraco.Sys.ServerVariables.umbracoSettings.keepUserLoggedIn !== true) {
- //NOTE: the safeApply because our timeout is set to not run digests (performance reasons)
- angularHelper.safeApply($rootScope, function () {
- try {
- //NOTE: We are calling this again so that the server can create a log that the timeout has expired, we
- // don't actually care about this result.
- authResource.getRemainingTimeoutSeconds();
- }
- finally {
- userAuthExpired();
- }
- });
+ if (success) {
+ securityRetryQueue.retryAll(currentUser.name);
}
else {
- //we've got less than 30 seconds remaining so let's check the server
-
- if (lastServerTimeoutSet != null) {
- //first we'll set the lastServerTimeoutSet to null - this is so we don't get back in to this loop while we
- // wait for a response from the server otherwise we'll be making double/triple/etc... calls while we wait.
- lastServerTimeoutSet = null;
-
- //now go get it from the server
- //NOTE: the safeApply because our timeout is set to not run digests (performance reasons)
- angularHelper.safeApply($rootScope, function () {
- authResource.getRemainingTimeoutSeconds().then(function (result) {
- setUserTimeoutInternal(result);
- });
- });
- }
-
- //recurse the countdown!
- countdownUserTimeout();
-
+ securityRetryQueue.cancelAll();
+ $location.path('/');
}
- }
}
- }, 5000, //every 5 seconds
- false); //false = do NOT execute a digest for every iteration
- }
- /** Called to update the current user's timeout */
- function setUserTimeoutInternal(newTimeout) {
-
-
- var asNumber = parseFloat(newTimeout);
- if (!isNaN(asNumber) && currentUser && angular.isNumber(asNumber)) {
- currentUser.remainingAuthSeconds = newTimeout;
- lastServerTimeoutSet = new Date();
- }
- }
-
- /** resets all user data, broadcasts the notAuthenticated event and shows the login dialog */
- function userAuthExpired(isLogout) {
- //store the last user id and clear the user
- if (currentUser && currentUser.id !== undefined) {
- lastUserId = currentUser.id;
- }
-
- if (currentUser) {
- currentUser.remainingAuthSeconds = 0;
- }
-
- lastServerTimeoutSet = null;
- currentUser = null;
-
- //broadcast a global event that the user is no longer logged in
- eventsService.emit("app.notAuthenticated");
-
- openLoginDialog(isLogout === undefined ? true : !isLogout);
- }
-
- // Register a handler for when an item is added to the retry queue
- securityRetryQueue.onItemAddedCallbacks.push(function (retryItem) {
- if (securityRetryQueue.hasMore()) {
- userAuthExpired();
- }
- });
-
- return {
-
- /** Internal method to display the login dialog */
- _showLoginDialog: function () {
- openLoginDialog();
- },
- /** Returns a promise, sends a request to the server to check if the current cookie is authorized */
- isAuthenticated: function () {
- //if we've got a current user then just return true
- if (currentUser) {
- var deferred = $q.defer();
- deferred.resolve(true);
- return deferred.promise;
+ /**
+ This methods will set the current user when it is resolved and
+ will then start the counter to count in-memory how many seconds they have
+ remaining on the auth session
+ */
+ function setCurrentUser(usr) {
+ if (!usr.remainingAuthSeconds) {
+ throw "The user object is invalid, the remainingAuthSeconds is required.";
+ }
+ currentUser = usr;
+ lastServerTimeoutSet = new Date();
+ //start the timer
+ countdownUserTimeout();
}
- return authResource.isAuthenticated();
- },
- /** Returns a promise, sends a request to the server to validate the credentials */
- authenticate: function (login, password) {
+ /**
+ Method to count down the current user's timeout seconds,
+ this will continually count down their current remaining seconds every 5 seconds until
+ there are no more seconds remaining.
+ */
+ function countdownUserTimeout() {
- return authResource.performLogin(login, password)
- .then(this.setAuthenticationSuccessful);
- },
- setAuthenticationSuccessful: function (data) {
+ $timeout(function () {
- //when it's successful, return the user data
- setCurrentUser(data);
+ if (currentUser) {
+ //countdown by 5 seconds since that is how long our timer is for.
+ currentUser.remainingAuthSeconds -= 5;
- var result = { user: data, authenticated: true, lastUserId: lastUserId, loginType: "credentials" };
+ //if there are more than 30 remaining seconds, recurse!
+ if (currentUser.remainingAuthSeconds > 30) {
- //broadcast a global event
- eventsService.emit("app.authenticated", result);
- return result;
- },
+ //we need to check when the last time the timeout was set from the server, if
+ // it has been more than 30 seconds then we'll manually go and retrieve it from the
+ // server - this helps to keep our local countdown in check with the true timeout.
+ if (lastServerTimeoutSet != null) {
+ var now = new Date();
+ var seconds = (now.getTime() - lastServerTimeoutSet.getTime()) / 1000;
- /** Logs the user out
- */
- logout: function () {
+ if (seconds > 30) {
- return authResource.performLogout()
- .then(function (data) {
- userAuthExpired();
- //done!
- return null;
- });
- },
+ //first we'll set the lastServerTimeoutSet to null - this is so we don't get back in to this loop while we
+ // wait for a response from the server otherwise we'll be making double/triple/etc... calls while we wait.
+ lastServerTimeoutSet = null;
- /** Refreshes the current user data with the data stored for the user on the server and returns it */
- refreshCurrentUser: function() {
- var deferred = $q.defer();
+ //now go get it from the server
+ //NOTE: the safeApply because our timeout is set to not run digests (performance reasons)
+ angularHelper.safeApply($rootScope, function () {
+ authResource.getRemainingTimeoutSeconds().then(function (result) {
+ setUserTimeoutInternal(result);
+ });
+ });
+ }
+ }
- authResource.getCurrentUser()
- .then(function (data) {
+ //recurse the countdown!
+ countdownUserTimeout();
+ }
+ else {
- var result = { user: data, authenticated: true, lastUserId: lastUserId, loginType: "implicit" };
-
- setCurrentUser(data);
+ //we are either timed out or very close to timing out so we need to show the login dialog.
+ if (Umbraco.Sys.ServerVariables.umbracoSettings.keepUserLoggedIn !== true) {
+ //NOTE: the safeApply because our timeout is set to not run digests (performance reasons)
+ angularHelper.safeApply($rootScope, function () {
+ try {
+ //NOTE: We are calling this again so that the server can create a log that the timeout has expired, we
+ // don't actually care about this result.
+ authResource.getRemainingTimeoutSeconds();
+ }
+ finally {
+ userAuthExpired();
+ }
+ });
+ }
+ else {
+ //we've got less than 30 seconds remaining so let's check the server
- deferred.resolve(currentUser);
- }, function () {
- //it failed, so they are not logged in
- deferred.reject();
- });
+ if (lastServerTimeoutSet != null) {
+ //first we'll set the lastServerTimeoutSet to null - this is so we don't get back in to this loop while we
+ // wait for a response from the server otherwise we'll be making double/triple/etc... calls while we wait.
+ lastServerTimeoutSet = null;
- return deferred.promise;
- },
+ //now go get it from the server
+ //NOTE: the safeApply because our timeout is set to not run digests (performance reasons)
+ angularHelper.safeApply($rootScope, function () {
+ authResource.getRemainingTimeoutSeconds().then(function (result) {
+ setUserTimeoutInternal(result);
+ });
+ });
+ }
- /** Returns the current user object in a promise */
- getCurrentUser: function (args) {
- var deferred = $q.defer();
+ //recurse the countdown!
+ countdownUserTimeout();
- if (!currentUser) {
- authResource.getCurrentUser()
- .then(function (data) {
+ }
+ }
+ }
+ }, 5000, //every 5 seconds
+ false); //false = do NOT execute a digest for every iteration
+ }
- var result = { user: data, authenticated: true, lastUserId: lastUserId, loginType: "implicit" };
+ /** Called to update the current user's timeout */
+ function setUserTimeoutInternal(newTimeout) {
- if (args && args.broadcastEvent) {
- //broadcast a global event, will inform listening controllers to load in the user specific data
+
+ var asNumber = parseFloat(newTimeout);
+ if (!isNaN(asNumber) && currentUser && angular.isNumber(asNumber)) {
+ currentUser.remainingAuthSeconds = newTimeout;
+ lastServerTimeoutSet = new Date();
+ }
+ }
+
+ /** resets all user data, broadcasts the notAuthenticated event and shows the login dialog */
+ function userAuthExpired(isLogout) {
+ //store the last user id and clear the user
+ if (currentUser && currentUser.id !== undefined) {
+ lastUserId = currentUser.id;
+ }
+
+ if (currentUser) {
+ currentUser.remainingAuthSeconds = 0;
+ }
+
+ lastServerTimeoutSet = null;
+ currentUser = null;
+
+ //broadcast a global event that the user is no longer logged in
+ eventsService.emit("app.notAuthenticated");
+
+ openLoginDialog(isLogout === undefined ? true : !isLogout);
+ }
+
+ // Register a handler for when an item is added to the retry queue
+ securityRetryQueue.onItemAddedCallbacks.push(function (retryItem) {
+ if (securityRetryQueue.hasMore()) {
+ userAuthExpired();
+ }
+ });
+
+ return {
+
+ /** Internal method to display the login dialog */
+ _showLoginDialog: function () {
+ openLoginDialog();
+ },
+ /** Returns a promise, sends a request to the server to check if the current cookie is authorized */
+ isAuthenticated: function () {
+ //if we've got a current user then just return true
+ if (currentUser) {
+ var deferred = $q.defer();
+ deferred.resolve(true);
+ return deferred.promise;
+ }
+ return authResource.isAuthenticated();
+ },
+
+ /** Returns a promise, sends a request to the server to validate the credentials */
+ authenticate: function (login, password) {
+
+ return authResource.performLogin(login, password)
+ .then(this.setAuthenticationSuccessful);
+ },
+ setAuthenticationSuccessful: function (data) {
+
+ //when it's successful, return the user data
+ setCurrentUser(data);
+
+ var result = { user: data, authenticated: true, lastUserId: lastUserId, loginType: "credentials" };
+
+ //broadcast a global event
eventsService.emit("app.authenticated", result);
- }
+ return result;
+ },
- setCurrentUser(data);
+ /** Logs the user out
+ */
+ logout: function () {
- deferred.resolve(currentUser);
- }, function () {
- //it failed, so they are not logged in
- deferred.reject();
- });
+ return authResource.performLogout()
+ .then(function (data) {
+ userAuthExpired();
+ //done!
+ return null;
+ });
+ },
- }
- else {
- deferred.resolve(currentUser);
- }
+ /** Refreshes the current user data with the data stored for the user on the server and returns it */
+ refreshCurrentUser: function () {
+ var deferred = $q.defer();
- return deferred.promise;
- },
+ authResource.getCurrentUser()
+ .then(function (data) {
- /** Called whenever a server request is made that contains a x-umb-user-seconds response header for which we can update the user's remaining timeout seconds */
- setUserTimeout: function (newTimeout) {
- setUserTimeoutInternal(newTimeout);
- }
- };
+ var result = { user: data, authenticated: true, lastUserId: lastUserId, loginType: "implicit" };
- });
+ setCurrentUser(data);
+
+ deferred.resolve(currentUser);
+ }, function () {
+ //it failed, so they are not logged in
+ deferred.reject();
+ });
+
+ return deferred.promise;
+ },
+
+ /** Returns the current user object in a promise */
+ getCurrentUser: function (args) {
+ var deferred = $q.defer();
+
+ if (!currentUser) {
+ authResource.getCurrentUser()
+ .then(function (data) {
+
+ var result = { user: data, authenticated: true, lastUserId: lastUserId, loginType: "implicit" };
+
+ if (args && args.broadcastEvent) {
+ //broadcast a global event, will inform listening controllers to load in the user specific data
+ eventsService.emit("app.authenticated", result);
+ }
+
+ setCurrentUser(data);
+
+ deferred.resolve(currentUser);
+ }, function () {
+ //it failed, so they are not logged in
+ deferred.reject();
+ });
+
+ }
+ else {
+ deferred.resolve(currentUser);
+ }
+
+ return deferred.promise;
+ },
+
+ /** Loads the Moment.js Locale for the current user. */
+ loadMomentLocaleForCurrentUser: function () {
+ var deferred = $q.defer();
+
+
+ function loadLocales(currentUser, supportedLocales) {
+ var locale = currentUser.locale.toLowerCase();
+ if (locale !== 'en-us') {
+ var localeUrls = [];
+ if (supportedLocales.indexOf(locale + '.js') > -1) {
+ localeUrls.push('lib/moment/' + locale + '.js');
+ }
+ if (locale.indexOf('-') > -1) {
+ var majorLocale = locale.split('-')[0] + '.js';
+ if (supportedLocales.indexOf(majorLocale) > -1) {
+ localeUrls.push('lib/moment/' + majorLocale);
+ }
+ }
+ assetsService.load(localeUrls).then(function () {
+ deferred.resolve(localeUrls);
+ });
+ } else {
+ deferred.resolve(['']);
+ }
+ }
+
+ var promises = {
+ currentUser: this.getCurrentUser(),
+ supportedLocales: javascriptLibraryService.getSupportedLocalesForMoment()
+ }
+
+ $q.all(promises).then(function (values) {
+ loadLocales(values.currentUser, values.supportedLocales);
+ });
+
+ return deferred.promise;
+
+ },
+
+ /** Called whenever a server request is made that contains a x-umb-user-seconds response header for which we can update the user's remaining timeout seconds */
+ setUserTimeout: function (newTimeout) {
+ setUserTimeoutInternal(newTimeout);
+ }
+ };
+
+ });
diff --git a/src/Umbraco.Web.UI.Client/src/init.js b/src/Umbraco.Web.UI.Client/src/init.js
index e86fa25c42..32462826fe 100644
--- a/src/Umbraco.Web.UI.Client/src/init.js
+++ b/src/Umbraco.Web.UI.Client/src/init.js
@@ -18,23 +18,26 @@ app.run(['userService', '$log', '$rootScope', '$location', 'queryStrings', 'navi
eventsService.on("app.authenticated", function(evt, data) {
assetsService._loadInitAssets().then(function() {
-
- //Register all of the tours on the server
- tourService.registerAllTours().then(function () {
- appReady(data);
-
- // Auto start intro tour
- tourService.getTourByAlias("umbIntroIntroduction").then(function (introTour) {
- // start intro tour if it hasn't been completed or disabled
- if (introTour && introTour.disabled !== true && introTour.completed !== true) {
- tourService.startTour(introTour);
- }
+
+ // Loads the user's locale settings for Moment.
+ userService.loadMomentLocaleForCurrentUser().then(function() {
+
+ //Register all of the tours on the server
+ tourService.registerAllTours().then(function () {
+ appReady(data);
+
+ // Auto start intro tour
+ tourService.getTourByAlias("umbIntroIntroduction").then(function (introTour) {
+ // start intro tour if it hasn't been completed or disabled
+ if (introTour && introTour.disabled !== true && introTour.completed !== true) {
+ tourService.startTour(introTour);
+ }
+ });
+
+ }, function(){
+ appReady(data);
});
-
- }, function(){
- appReady(data);
});
-
});
});
diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less
index d2a80d93aa..fb31889fd0 100644
--- a/src/Umbraco.Web.UI.Client/src/less/belle.less
+++ b/src/Umbraco.Web.UI.Client/src/less/belle.less
@@ -168,6 +168,9 @@
//used for property editors
@import "property-editors.less";
+//used for prevalue editors
+@import "components/prevalues/multivalues.less";
+
@import "typeahead.less";
@import "hacks.less";
@@ -175,4 +178,4 @@
@import "healthcheck.less";
// cleanup properties.less when it is done
-@import "properties.less";
\ No newline at end of file
+@import "properties.less";
diff --git a/src/Umbraco.Web.UI.Client/src/less/components/prevalues/multivalues.less b/src/Umbraco.Web.UI.Client/src/less/components/prevalues/multivalues.less
new file mode 100644
index 0000000000..a307e5c585
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/less/components/prevalues/multivalues.less
@@ -0,0 +1,53 @@
+.umb-prevalues-multivalues {
+ width: 400px;
+}
+
+.umb-prevalues-multivalues__left {
+ display: flex;
+ flex: 1 1 auto;
+}
+
+.umb-prevalues-multivalues__right {
+ display: flex;
+ flex: 0 0 auto;
+ align-items: center;
+}
+
+.umb-prevalues-multivalues__add {
+ display: flex;
+}
+
+.umb-prevalues-multivalues__add input {
+ width: 320px;
+}
+
+.umb-prevalues-multivalues__add input {
+ display: flex;
+}
+
+.umb-prevalues-multivalues__add button {
+ margin: 0 6px 0 0;
+ float: right
+}
+
+.umb-prevalues-multivalues__listitem {
+ display: flex;
+ padding: 6px;
+ margin: 10px 0px !important;
+ background: #F3F3F5;
+ cursor: move;
+}
+
+.umb-prevalues-multivalues__listitem i {
+ display: flex;
+ align-items: center;
+ margin-right: 5px
+}
+
+.umb-prevalues-multivalues__listitem a {
+ cursor: pointer;
+}
+
+.umb-prevalues-multivalues__listitem input {
+ width: 295px;
+}
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html
index 564d50951e..f2be5604ed 100644
--- a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html
+++ b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html
@@ -171,27 +171,25 @@
{{publishStatus.label}}
-
+
{{node.createDateFormatted}} by {{ node.owner.name }}
-
+
-
-
-
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.controller.js b/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.controller.js
index 4e734b76a6..aade96c3cc 100644
--- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.controller.js
@@ -14,18 +14,21 @@ function DocumentTypesCreateController($scope, $location, navigationService, con
creatingFolder: false,
};
+ var disableTemplates = Umbraco.Sys.ServerVariables.features.disabledFeatures.disableTemplates;
+ $scope.model.disableTemplates = disableTemplates;
+
var node = $scope.dialogOptions.currentNode,
localizeCreateFolder = localizationService.localize("defaultdialog_createFolder");
- $scope.showCreateFolder = function() {
+ $scope.showCreateFolder = function () {
$scope.model.creatingFolder = true;
};
- $scope.createContainer = function() {
+ $scope.createContainer = function () {
- if (formHelper.submitForm({scope: $scope, formCtrl: this.createFolderForm, statusMessage: localizeCreateFolder})) {
+ if (formHelper.submitForm({ scope: $scope, formCtrl: this.createFolderForm, statusMessage: localizeCreateFolder })) {
- contentTypeResource.createContainer(node.id, $scope.model.folderName).then(function(folderId) {
+ contentTypeResource.createContainer(node.id, $scope.model.folderName).then(function (folderId) {
navigationService.hideMenu();
@@ -44,7 +47,7 @@ function DocumentTypesCreateController($scope, $location, navigationService, con
var section = appState.getSectionState("currentSection");
- }, function(err) {
+ }, function (err) {
$scope.error = err;
@@ -58,14 +61,17 @@ function DocumentTypesCreateController($scope, $location, navigationService, con
}
};
- $scope.createDocType = function() {
- $location.search('create', null);
- $location.search('notemplate', null);
- $location.path("/settings/documenttypes/edit/" + node.id).search("create", "true");
- navigationService.hideMenu();
- };
+ // Disabling logic for creating document type with template if disableTemplates is set to true
+ if (!disableTemplates) {
+ $scope.createDocType = function () {
+ $location.search('create', null);
+ $location.search('notemplate', null);
+ $location.path("/settings/documenttypes/edit/" + node.id).search("create", "true");
+ navigationService.hideMenu();
+ };
+ }
- $scope.createComponent = function() {
+ $scope.createComponent = function () {
$location.search('create', null);
$location.search('notemplate', null);
$location.path("/settings/documenttypes/edit/" + node.id).search("create", "true").search("notemplate", "true");
diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.html b/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.html
index b61a4c014e..e5043be785 100644
--- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.html
+++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.html
@@ -4,7 +4,7 @@