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.dll True + ..\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 }} - + - - @@ -200,7 +198,7 @@
{{ node.id }}
{{ node.key }} - + 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 @@
Create an item under {{currentNode.name}}
- -
-

Select

@@ -112,7 +106,6 @@
-
@@ -134,7 +127,6 @@
- - -
- -

Styles saved and published

- - - diff --git a/src/Umbraco.Web.UI.Client/src/canvasdesigner/editors/layout.html b/src/Umbraco.Web.UI/Umbraco/Views/Preview/Layout.cshtml similarity index 88% rename from src/Umbraco.Web.UI.Client/src/canvasdesigner/editors/layout.html rename to src/Umbraco.Web.UI/Umbraco/Views/Preview/Layout.cshtml index 5a0dfed448..3df51ae23f 100644 --- a/src/Umbraco.Web.UI.Client/src/canvasdesigner/editors/layout.html +++ b/src/Umbraco.Web.UI/Umbraco/Views/Preview/Layout.cshtml @@ -1,4 +1,4 @@ - +@inherits System.Web.Mvc.WebViewPage
@@ -7,4 +7,4 @@ Full
-
\ No newline at end of file + diff --git a/src/Umbraco.Web.UI.Client/src/canvasdesigner/editors/margin.html b/src/Umbraco.Web.UI/Umbraco/Views/Preview/Margin.cshtml similarity index 84% rename from src/Umbraco.Web.UI.Client/src/canvasdesigner/editors/margin.html rename to src/Umbraco.Web.UI/Umbraco/Views/Preview/Margin.cshtml index 145c38f2f3..f14e592420 100644 --- a/src/Umbraco.Web.UI.Client/src/canvasdesigner/editors/margin.html +++ b/src/Umbraco.Web.UI/Umbraco/Views/Preview/Margin.cshtml @@ -1,7 +1,7 @@ - +@inherits System.Web.Mvc.WebViewPage
-
+
@@ -11,4 +11,4 @@
-
\ No newline at end of file +
diff --git a/src/Umbraco.Web.UI.Client/src/canvasdesigner/editors/padding.html b/src/Umbraco.Web.UI/Umbraco/Views/Preview/Padding.cshtml similarity index 91% rename from src/Umbraco.Web.UI.Client/src/canvasdesigner/editors/padding.html rename to src/Umbraco.Web.UI/Umbraco/Views/Preview/Padding.cshtml index e645ce0a67..3f2c440945 100644 --- a/src/Umbraco.Web.UI.Client/src/canvasdesigner/editors/padding.html +++ b/src/Umbraco.Web.UI/Umbraco/Views/Preview/Padding.cshtml @@ -1,4 +1,4 @@ - +@inherits System.Web.Mvc.WebViewPage
@@ -11,4 +11,4 @@
-
\ No newline at end of file + diff --git a/src/Umbraco.Web.UI.Client/src/canvasdesigner/editors/radius.html b/src/Umbraco.Web.UI/Umbraco/Views/Preview/Radius.cshtml similarity index 95% rename from src/Umbraco.Web.UI.Client/src/canvasdesigner/editors/radius.html rename to src/Umbraco.Web.UI/Umbraco/Views/Preview/Radius.cshtml index 328698b44b..1e8a96b712 100644 --- a/src/Umbraco.Web.UI.Client/src/canvasdesigner/editors/radius.html +++ b/src/Umbraco.Web.UI/Umbraco/Views/Preview/Radius.cshtml @@ -1,9 +1,9 @@ - +@inherits System.Web.Mvc.WebViewPage
    - +
  • @@ -18,4 +18,4 @@
-
\ No newline at end of file + diff --git a/src/Umbraco.Web.UI.Client/src/canvasdesigner/editors/shadow.html b/src/Umbraco.Web.UI/Umbraco/Views/Preview/Shadow.cshtml similarity index 80% rename from src/Umbraco.Web.UI.Client/src/canvasdesigner/editors/shadow.html rename to src/Umbraco.Web.UI/Umbraco/Views/Preview/Shadow.cshtml index f9f88401c4..6b9d4763e8 100644 --- a/src/Umbraco.Web.UI.Client/src/canvasdesigner/editors/shadow.html +++ b/src/Umbraco.Web.UI/Umbraco/Views/Preview/Shadow.cshtml @@ -1,8 +1,8 @@ - +@inherits System.Web.Mvc.WebViewPage
-
\ No newline at end of file + diff --git a/src/Umbraco.Web.UI.Client/src/canvasdesigner/editors/slider.html b/src/Umbraco.Web.UI/Umbraco/Views/Preview/Slider.cshtml similarity index 82% rename from src/Umbraco.Web.UI.Client/src/canvasdesigner/editors/slider.html rename to src/Umbraco.Web.UI/Umbraco/Views/Preview/Slider.cshtml index f33d314841..414159d714 100644 --- a/src/Umbraco.Web.UI.Client/src/canvasdesigner/editors/slider.html +++ b/src/Umbraco.Web.UI/Umbraco/Views/Preview/Slider.cshtml @@ -1,8 +1,8 @@ - +@inherits System.Web.Mvc.WebViewPage
-
\ No newline at end of file + diff --git a/src/Umbraco.Web.UI/Umbraco/Views/Preview/web.config b/src/Umbraco.Web.UI/Umbraco/Views/Preview/web.config new file mode 100644 index 0000000000..5ab8bbc7d6 --- /dev/null +++ b/src/Umbraco.Web.UI/Umbraco/Views/Preview/web.config @@ -0,0 +1,41 @@ + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Umbraco.Web/Cache/ContentCacheRefresher.cs b/src/Umbraco.Web/Cache/ContentCacheRefresher.cs index 7d33630432..257e0c9731 100644 --- a/src/Umbraco.Web/Cache/ContentCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/ContentCacheRefresher.cs @@ -6,6 +6,7 @@ using Umbraco.Core.Configuration; using Umbraco.Core.Models; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.Repositories.Implement; +using Umbraco.Core.Services; using Umbraco.Core.Services.Changes; using Umbraco.Web.Composing; using Umbraco.Web.PublishedCache; @@ -15,11 +16,13 @@ namespace Umbraco.Web.Cache public sealed class ContentCacheRefresher : PayloadCacheRefresherBase { private readonly IPublishedSnapshotService _publishedSnapshotService; + private readonly IdkMap _idkMap; - public ContentCacheRefresher(CacheHelper cacheHelper, IPublishedSnapshotService publishedSnapshotService) + public ContentCacheRefresher(CacheHelper cacheHelper, IPublishedSnapshotService publishedSnapshotService, IdkMap idkMap) : base(cacheHelper) { _publishedSnapshotService = publishedSnapshotService; + _idkMap = idkMap; } #region Define @@ -51,6 +54,8 @@ namespace Umbraco.Web.Cache // remove that one runtimeCache.ClearCacheItem(RepositoryCacheKeys.GetKey(payload.Id)); + _idkMap.ClearCache(payload.Id); + // remove those that are in the branch if (payload.ChangeTypes.HasTypesAny(TreeChangeTypes.RefreshBranch | TreeChangeTypes.Remove)) { diff --git a/src/Umbraco.Web/Cache/MediaCacheRefresher.cs b/src/Umbraco.Web/Cache/MediaCacheRefresher.cs index 09a4e3eb5d..2fbc0418cf 100644 --- a/src/Umbraco.Web/Cache/MediaCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/MediaCacheRefresher.cs @@ -41,6 +41,8 @@ namespace Umbraco.Web.Cache public override void Refresh(JsonPayload[] payloads) { + if (payloads == null) return; + bool anythingChanged; _publishedSnapshotService.Notify(payloads, out anythingChanged); @@ -52,7 +54,11 @@ namespace Umbraco.Web.Cache foreach (var payload in payloads) { - _idkMap.ClearCache(payload.Id); + if (payload.Operation == OperationType.Deleted) + _idkMap.ClearCache(payload.Id); + + + // note: ClearCacheByKeySearch - does StartsWith(...) diff --git a/src/Umbraco.Web/Editors/BackOfficeAssetsController.cs b/src/Umbraco.Web/Editors/BackOfficeAssetsController.cs new file mode 100644 index 0000000000..5a5f6fc5e2 --- /dev/null +++ b/src/Umbraco.Web/Editors/BackOfficeAssetsController.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web.Http; +using Umbraco.Core.IO; +using Umbraco.Web.Mvc; + +namespace Umbraco.Web.Editors +{ + [PluginController("UmbracoApi")] + public class BackOfficeAssetsController : UmbracoAuthorizedJsonController + { + + [HttpGet] + public IEnumerable GetSupportedMomentLocales() + { + const string momentLocaleFolder = "moment"; + var fileSystem = FileSystemProviderManager.Current.JavaScriptLibraryFileSystem; + var cultures = fileSystem.GetFiles(momentLocaleFolder, "*.js").ToList(); + for (var i = 0; i < cultures.Count; i++) + { + cultures[i] = cultures[i] + .Substring(cultures[i].IndexOf(momentLocaleFolder, StringComparison.Ordinal) + momentLocaleFolder.Length + 1); + } + return cultures; + } + } +} diff --git a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs index 6d080ac048..69aea82add 100644 --- a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs @@ -14,6 +14,7 @@ using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; +using Umbraco.Web.Features; using Umbraco.Web.HealthCheck; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Mvc; @@ -53,7 +54,8 @@ namespace Umbraco.Web.Editors {"umbracoUrls", new[] {"authenticationApiBaseUrl", "serverVarsJs", "externalLoginsUrl", "currentUserApiBaseUrl"}}, {"umbracoSettings", new[] {"allowPasswordReset", "imageFileTypes", "maxFileSize", "loginBackgroundImage"}}, {"application", new[] {"applicationPath", "cacheBuster"}}, - {"isDebuggingEnabled", new string[] { }} + {"isDebuggingEnabled", new string[] { }}, + {"features", new [] {"disabledFeatures"}} }; //now do the filtering... var defaults = GetServerVariables(); @@ -82,7 +84,7 @@ namespace Umbraco.Web.Editors //TODO: This is ultra confusing! this same key is used for different things, when returning the full app when authenticated it is this URL but when not auth'd it's actually the ServerVariables address // so based on compat and how things are currently working we need to replace the serverVarsJs one - ((Dictionary) defaults["umbracoUrls"])["serverVarsJs"] = _urlHelper.Action("ServerVariables", "BackOffice"); + ((Dictionary)defaults["umbracoUrls"])["serverVarsJs"] = _urlHelper.Action("ServerVariables", "BackOffice"); return defaults; } @@ -277,6 +279,10 @@ namespace Umbraco.Web.Editors { "helpApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( controller => controller.GetContextHelpForPage("","","")) + }, + { + "backOfficeAssetsApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( + controller => controller.GetSupportedMomentLocales()) } } }, @@ -337,6 +343,18 @@ namespace Umbraco.Web.Editors .ToArray() } } + }, + { + "features", new Dictionary + { + { + "disabledFeatures", new Dictionary + { + { "disableTemplates", FeaturesResolver.Current.Features.Disabled.DisableTemplates} + } + } + + } } }; return defaultVals; @@ -361,9 +379,9 @@ namespace Umbraco.Web.Editors .ToArray(); return (from p in pluginTreesWithAttributes - let treeAttr = p.attributes.OfType().Single() - let pluginAttr = p.attributes.OfType().Single() - select new Dictionary + let treeAttr = p.attributes.OfType().Single() + let pluginAttr = p.attributes.OfType().Single() + select new Dictionary { {"alias", treeAttr.Alias}, {"packageFolder", pluginAttr.AreaName} }).ToArray(); diff --git a/src/Umbraco.Web/Editors/PreviewController.cs b/src/Umbraco.Web/Editors/PreviewController.cs new file mode 100644 index 0000000000..08de0f83ba --- /dev/null +++ b/src/Umbraco.Web/Editors/PreviewController.cs @@ -0,0 +1,41 @@ +using System; +using System.Web.Mvc; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Web.Features; +using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.Mvc; + +namespace Umbraco.Web.Editors +{ + [DisableBrowserCache] + public class PreviewController : Controller + { + [UmbracoAuthorize(redirectToUmbracoLogin: true)] + public ActionResult Index() + { + var model = new BackOfficePreview + { + DisableDevicePreview = FeaturesResolver.Current.Features.Disabled.DisableDevicePreview, + PreviewExtendedHeaderView = FeaturesResolver.Current.Features.Enabled.PreviewExtendedView + }; + + if (model.PreviewExtendedHeaderView.IsNullOrWhiteSpace() == false) + { + var viewEngineResult = ViewEngines.Engines.FindPartialView(ControllerContext, model.PreviewExtendedHeaderView); + if (viewEngineResult.View == null) + { + throw new InvalidOperationException("Could not find the view " + model.PreviewExtendedHeaderView + ", the following locations were searched: " + Environment.NewLine + string.Join(Environment.NewLine, viewEngineResult.SearchedLocations)); + } + } + + return View(GlobalSettings.Path.EnsureEndsWith('/') + "Views/Preview/" + "Index.cshtml", model); + } + + public ActionResult Editors(string editor) + { + if (string.IsNullOrEmpty(editor)) throw new ArgumentNullException("editor"); + return View(GlobalSettings.Path.EnsureEndsWith('/') + "Views/Preview/" + editor.Replace(".html", string.Empty) + ".cshtml"); + } + } +} diff --git a/src/Umbraco.Web/Editors/TourController.cs b/src/Umbraco.Web/Editors/TourController.cs index 8ac62922ac..97208c88e5 100644 --- a/src/Umbraco.Web/Editors/TourController.cs +++ b/src/Umbraco.Web/Editors/TourController.cs @@ -73,14 +73,20 @@ namespace Umbraco.Web.Editors //Checking to see if the user has access to the required tour sections, else we remove the tour foreach (var backOfficeTourFile in result) { - foreach (var tour in backOfficeTourFile.Tours) + if (backOfficeTourFile.Tours != null) { - foreach (var toursRequiredSection in tour.RequiredSections) + foreach (var tour in backOfficeTourFile.Tours) { - if (allowedSections.Contains(toursRequiredSection) == false) + if (tour.RequiredSections != null) { - toursToBeRemoved.Add(backOfficeTourFile); - break; + foreach (var toursRequiredSection in tour.RequiredSections) + { + if (allowedSections.Contains(toursRequiredSection) == false) + { + toursToBeRemoved.Add(backOfficeTourFile); + break; + } + } } } } diff --git a/src/Umbraco.Web/Features/DisabledFeatures.cs b/src/Umbraco.Web/Features/DisabledFeatures.cs index f0efa28739..62fb019c70 100644 --- a/src/Umbraco.Web/Features/DisabledFeatures.cs +++ b/src/Umbraco.Web/Features/DisabledFeatures.cs @@ -20,5 +20,16 @@ namespace Umbraco.Web.Features /// Gets the disabled controllers. /// public TypeList Controllers { get; } + + /// + /// Disables the device preview feature of previewing. + /// + public bool DisableDevicePreview { get; set; } + + /// + /// If true, all references to templates will be removed in the back office and routing + /// + public bool DisableTemplates { get; set; } + } } diff --git a/src/Umbraco.Web/Features/EnabledFeatures.cs b/src/Umbraco.Web/Features/EnabledFeatures.cs index 7f0d047992..7e734f6a84 100644 --- a/src/Umbraco.Web/Features/EnabledFeatures.cs +++ b/src/Umbraco.Web/Features/EnabledFeatures.cs @@ -6,9 +6,11 @@ namespace Umbraco.Web.Features internal class EnabledFeatures { /// - /// Specifies if rendering pipeline should ignore HasTemplate check when handling a request. - /// This is to allow JSON preview of content with no template set. + /// This allows us to inject a razor view into the Umbraco preview view to extend it /// - public bool RenderNoTemplate { get; set; } + /// + /// This is set to a virtual path of a razor view file + /// + public string PreviewExtendedView { get; set; } } } diff --git a/src/Umbraco.Web/Features/UmbracoFeatures.cs b/src/Umbraco.Web/Features/UmbracoFeatures.cs index ecdeb5dd9a..8bd0017d0f 100644 --- a/src/Umbraco.Web/Features/UmbracoFeatures.cs +++ b/src/Umbraco.Web/Features/UmbracoFeatures.cs @@ -16,14 +16,7 @@ namespace Umbraco.Web.Features Disabled = new DisabledFeatures(); Enabled = new EnabledFeatures(); } - - // note - // currently, the only thing a FeatureSet does is list disabled controllers, - // but eventually we could enable and disable more parts of Umbraco. and then - // we would need some logic to figure out what's enabled/disabled - hence it's - // better to use IsEnabled, where the logic would go, rather than directly - // accessing the Disabled collection. - + /// /// Gets the disabled features. /// @@ -35,9 +28,9 @@ namespace Umbraco.Web.Features public EnabledFeatures Enabled { get; } /// - /// Determines whether a feature is enabled. + /// Determines whether a controller is enabled. /// - public bool IsEnabled(Type feature) + public bool IsControllerEnabled(Type feature) { if (typeof(UmbracoApiControllerBase).IsAssignableFrom(feature)) return Disabled.Controllers.Contains(feature) == false; diff --git a/src/Umbraco.Web/Models/BackOfficeTour.cs b/src/Umbraco.Web/Models/BackOfficeTour.cs index 78a4cd1897..268d5667f4 100644 --- a/src/Umbraco.Web/Models/BackOfficeTour.cs +++ b/src/Umbraco.Web/Models/BackOfficeTour.cs @@ -9,6 +9,11 @@ namespace Umbraco.Web.Models [DataContract(Name = "tour", Namespace = "")] public class BackOfficeTour { + public BackOfficeTour() + { + RequiredSections = new List(); + } + [DataMember(Name = "name")] public string Name { get; set; } [DataMember(Name = "alias")] diff --git a/src/Umbraco.Web/Models/BackOfficeTourFile.cs b/src/Umbraco.Web/Models/BackOfficeTourFile.cs index 7291a89ff4..6840171f48 100644 --- a/src/Umbraco.Web/Models/BackOfficeTourFile.cs +++ b/src/Umbraco.Web/Models/BackOfficeTourFile.cs @@ -9,6 +9,11 @@ namespace Umbraco.Web.Models [DataContract(Name = "tourFile", Namespace = "")] public class BackOfficeTourFile { + public BackOfficeTourFile() + { + Tours = new List(); + } + /// /// The file name for the tour /// @@ -27,4 +32,4 @@ namespace Umbraco.Web.Models [DataMember(Name = "tours")] public IEnumerable Tours { get; set; } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web/Models/ContentEditing/AuditLog.cs b/src/Umbraco.Web/Models/ContentEditing/AuditLog.cs index 3b945f40d9..9210472313 100644 --- a/src/Umbraco.Web/Models/ContentEditing/AuditLog.cs +++ b/src/Umbraco.Web/Models/ContentEditing/AuditLog.cs @@ -6,6 +6,7 @@ using Umbraco.Core.Models; namespace Umbraco.Web.Models.ContentEditing { + [DataContract(Name = "auditLog", Namespace = "")] public class AuditLog { diff --git a/src/Umbraco.Web/Models/ContentEditing/BackOfficePreview.cs b/src/Umbraco.Web/Models/ContentEditing/BackOfficePreview.cs new file mode 100644 index 0000000000..ed2e623226 --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/BackOfficePreview.cs @@ -0,0 +1,12 @@ +namespace Umbraco.Web.Models.ContentEditing +{ + /// + /// The model representing Previewing of a content item from the back office + /// + public class BackOfficePreview + { + public string PreviewExtendedHeaderView { get; set; } + //TODO: We could potentially have a 'footer' view + public bool DisableDevicePreview { get; set; } + } +} diff --git a/src/Umbraco.Web/Mvc/BackOfficeArea.cs b/src/Umbraco.Web/Mvc/BackOfficeArea.cs index fe4a9d6c2b..34ac171bd0 100644 --- a/src/Umbraco.Web/Mvc/BackOfficeArea.cs +++ b/src/Umbraco.Web/Mvc/BackOfficeArea.cs @@ -1,10 +1,6 @@ -using System.Web; -using System.Web.Mvc; -using System.Web.Routing; +using System.Web.Mvc; using Umbraco.Core.Configuration; using Umbraco.Web.Editors; -using Umbraco.Web.Install; -using Umbraco.Web.Install.Controllers; namespace Umbraco.Web.Mvc { @@ -24,6 +20,12 @@ namespace Umbraco.Web.Mvc /// public override void RegisterArea(AreaRegistrationContext context) { + context.MapRoute( + "Umbraco_preview", + GlobalSettings.UmbracoMvcArea + "/preview/{action}/{editor}", + new {controller = "Preview", action = "Index", editor = UrlParameter.Optional}, + new[] { "Umbraco.Web.Editors" }); + context.MapRoute( "Umbraco_back_office", GlobalSettings.UmbracoMvcArea + "/{action}/{id}", diff --git a/src/Umbraco.Web/Mvc/RenderRouteHandler.cs b/src/Umbraco.Web/Mvc/RenderRouteHandler.cs index 60888221d5..71fca39eb0 100644 --- a/src/Umbraco.Web/Mvc/RenderRouteHandler.cs +++ b/src/Umbraco.Web/Mvc/RenderRouteHandler.cs @@ -391,9 +391,12 @@ namespace Umbraco.Web.Mvc return GetWebFormsHandler(); } - //here we need to check if there is no hijacked route and no template assigned, if this is the case - //we want to return a blank page, but we'll leave that up to the NoTemplateHandler. - if (!request.HasTemplate && !routeDef.HasHijackedRoute && !Features.Enabled.RenderNoTemplate) + //Here we need to check if there is no hijacked route and no template assigned, + //if this is the case we want to return a blank page, but we'll leave that up to the NoTemplateHandler. + //We also check if templates have been disabled since if they are then we're allowed to render even though there's no template, + //for example for json rendering in headless. + if ((request.HasTemplate == false && Features.Disabled.DisableTemplates == false) + && routeDef.HasHijackedRoute == false) { // fixme - better find a way to inject that engine? or at least Current.Engine of some sort! var engine = Core.Composing.Current.Container.GetInstance(); diff --git a/src/Umbraco.Web/Mvc/UmbracoAuthorizeAttribute.cs b/src/Umbraco.Web/Mvc/UmbracoAuthorizeAttribute.cs index e94cffc8c4..1839d47ad9 100644 --- a/src/Umbraco.Web/Mvc/UmbracoAuthorizeAttribute.cs +++ b/src/Umbraco.Web/Mvc/UmbracoAuthorizeAttribute.cs @@ -3,6 +3,7 @@ using System.Web; using System.Web.Mvc; using Umbraco.Core; using Umbraco.Web.Composing; +using Umbraco.Core.Configuration; namespace Umbraco.Web.Mvc { @@ -14,6 +15,7 @@ namespace Umbraco.Web.Mvc // see note in HttpInstallAuthorizeAttribute private readonly UmbracoContext _umbracoContext; private readonly IRuntimeState _runtimeState; + private readonly string _redirectUrl; private IRuntimeState RuntimeState => _runtimeState ?? Current.RuntimeState; @@ -32,9 +34,33 @@ namespace Umbraco.Web.Mvc _runtimeState = runtimeState; } + /// + /// Default constructor + /// public UmbracoAuthorizeAttribute() { } + /// + /// Constructor specifying to redirect to the specified location if not authorized + /// + /// + public UmbracoAuthorizeAttribute(string redirectUrl) + { + _redirectUrl = redirectUrl ?? throw new ArgumentNullException(nameof(redirectUrl)); + } + + /// + /// Constructor specifying to redirect to the umbraco login page if not authorized + /// + /// + public UmbracoAuthorizeAttribute(bool redirectToUmbracoLogin) + { + if (redirectToUmbracoLogin) + { + _redirectUrl = GlobalSettings.Path.EnsureStartsWith("~"); + } + } + /// /// Ensures that the user must be in the Administrator or the Install role /// @@ -64,7 +90,16 @@ namespace Umbraco.Web.Mvc /// protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext) { - filterContext.Result = new HttpUnauthorizedResult("You must login to view this resource."); + if (_redirectUrl.IsNullOrWhiteSpace()) + { + filterContext.Result = (ActionResult)new HttpUnauthorizedResult("You must login to view this resource."); + + + } + else + { + filterContext.Result = new RedirectResult(_redirectUrl); + } // DON'T do a FormsAuth redirect... argh!! thankfully we're running .Net 4.5 :) filterContext.RequestContext.HttpContext.Response.SuppressFormsAuthenticationRedirect = true; diff --git a/src/Umbraco.Web/PublishedContentQuery.cs b/src/Umbraco.Web/PublishedContentQuery.cs index 2a8bedc79f..cd378b214a 100644 --- a/src/Umbraco.Web/PublishedContentQuery.cs +++ b/src/Umbraco.Web/PublishedContentQuery.cs @@ -8,6 +8,7 @@ using Examine.LuceneEngine.SearchCriteria; using Examine.SearchCriteria; using Umbraco.Core; using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Services; using Umbraco.Core.Xml; using Umbraco.Web.PublishedCache; @@ -171,7 +172,6 @@ namespace Umbraco.Web private static IPublishedContent ItemById(Guid id, IPublishedCache cache) { var doc = cache.GetById(id); - return doc; } private static IPublishedContent ItemByXPath(string xpath, XPathVariable[] vars, IPublishedCache cache) @@ -243,13 +243,13 @@ namespace Umbraco.Web if (!(searcher is BaseLuceneSearcher luceneSearcher)) { var results = searcher.Search(term, useWildCards); - totalRecords = results.TotalItemCount; + totalRecords = results.TotalItemCount; // Examine skip, Linq take return results.Skip(skip).ToPublishedSearchResults(_contentCache).Take(take); } var criteria = SearchAllFields(term, useWildCards, luceneSearcher); - return Search(skip, take, out totalRecords, criteria, searcher); + return Search(skip, take, out totalRecords, criteria, searcher); } /// @@ -287,7 +287,7 @@ namespace Umbraco.Web { //get the GetSearchFields method from BaseLuceneSearcher _examineGetSearchFields = typeof(BaseLuceneSearcher).GetMethod("GetSearchFields", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static); - } + } //get the results of searcher.BaseLuceneSearcher() using ugly reflection since it's not public var searchFields = (IEnumerable) _examineGetSearchFields.Invoke(searcher, null); @@ -314,8 +314,8 @@ namespace Umbraco.Web public Examineness Examineness { get; private set; } public string Value { get; private set; } public float Level { get; private set; } - } - + } + #endregion } } diff --git a/src/Umbraco.Web/UI/JavaScript/JsInitialize.js b/src/Umbraco.Web/UI/JavaScript/JsInitialize.js index 961fb8e54e..39185cff3f 100644 --- a/src/Umbraco.Web/UI/JavaScript/JsInitialize.js +++ b/src/Umbraco.Web/UI/JavaScript/JsInitialize.js @@ -3,7 +3,7 @@ 'lib/angular/1.1.5/angular.min.js', 'lib/underscore/underscore-min.js', - 'lib/moment/moment-with-locales.js', + 'lib/moment/moment.min.js', 'lib/jquery-ui/jquery-ui.min.js', 'lib/jquery-ui-touch-punch/jquery.ui.touch-punch.js', diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 5ba647ddff..b9d805ce69 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -49,6 +49,12 @@ + + ..\packages\System.Threading.Tasks.Dataflow.4.7.0\lib\portable-net45+win8+wpa81\System.Threading.Tasks.Dataflow.dll + + + ..\packages\System.ValueTuple.4.4.0\lib\netstandard1.0\System.ValueTuple.dll + @@ -128,6 +134,15 @@ + + + + + + + + + @@ -143,6 +158,15 @@ + + + + + + + + + diff --git a/src/Umbraco.Web/WebApi/Filters/FeatureAuthorizeAttribute.cs b/src/Umbraco.Web/WebApi/Filters/FeatureAuthorizeAttribute.cs index 60afe1f8cb..0046770599 100644 --- a/src/Umbraco.Web/WebApi/Filters/FeatureAuthorizeAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/FeatureAuthorizeAttribute.cs @@ -23,7 +23,7 @@ namespace Umbraco.Web.WebApi.Filters if (features == null) return true; var controllerType = actionContext.ControllerContext.ControllerDescriptor.ControllerType; - return features.IsEnabled(controllerType); + return features.IsControllerEnabled(controllerType); } } } diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/create/MemberGroupTasks.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/create/MemberGroupTasks.cs index ecbd8d981f..55a8930f83 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/create/MemberGroupTasks.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/create/MemberGroupTasks.cs @@ -1,21 +1,24 @@ +using System.Linq; using System.Web.Security; using Umbraco.Core; using Umbraco.Web.Composing; using Umbraco.Web._Legacy.UI; -namespace umbraco +namespace Umbraco.Web.umbraco.presentation.umbraco.create { public class MemberGroupTasks : LegacyDialogTask { public override bool PerformSave() { Roles.CreateRole(Alias); - _returnUrl = string.Format("members/EditMemberGroup.aspx?id={0}", System.Web.HttpContext.Current.Server.UrlEncode(Alias)); + _returnUrl = $"members/EditMemberGroup.aspx?id={System.Web.HttpContext.Current.Server.UrlEncode(Alias)}"; return true; } public override bool PerformDelete() { + var roleDeleted = false; + // only built-in roles can be deleted if (Roles.Provider.Name == Constants.Conventions.Member.UmbracoRoleProviderName) { @@ -24,21 +27,31 @@ namespace umbraco { Current.Services.MemberGroupService.Delete(group); } - return true; } - return false; + + // Need to delete the member group from any content item that has it assigned in public access settings + var publicAccessService = UmbracoContext.Current.Application.Services.PublicAccessService; + var allPublicAccessRules = publicAccessService.GetAll(); + + // Find only rules which have the current role name (alias) assigned to them + var rulesWithDeletedRoles = allPublicAccessRules.Where(x => x.Rules.Any(r => r.RuleValue == Alias)); + + var contentService = UmbracoContext.Current.Application.Services.ContentService; + foreach (var publicAccessEntry in rulesWithDeletedRoles) + { + var contentItem = contentService.GetById(publicAccessEntry.ProtectedNodeId); + var rulesToDelete = publicAccessEntry.Rules.ToList(); + foreach (var rule in rulesToDelete) + publicAccessService.RemoveRule(contentItem, rule.RuleType, rule.RuleValue); + } + + return roleDeleted; } private string _returnUrl = ""; - public override string ReturnUrl - { - get { return _returnUrl; } - } + public override string ReturnUrl => _returnUrl; - public override string AssignedApp - { - get { return Constants.Applications.Members.ToString(); } - } + public override string AssignedApp => Constants.Applications.Members.ToString(); } }