diff --git a/src/Umbraco.Core/ApplicationContext.cs b/src/Umbraco.Core/ApplicationContext.cs index ca54afb7b1..82ef562bc1 100644 --- a/src/Umbraco.Core/ApplicationContext.cs +++ b/src/Umbraco.Core/ApplicationContext.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Configuration; using System.Threading; using Semver; @@ -261,7 +262,7 @@ namespace Umbraco.Core /// - http://issues.umbraco.org/issue/U4-5728 /// - http://issues.umbraco.org/issue/U4-5391 /// - internal string UmbracoApplicationUrl + public string UmbracoApplicationUrl { get { @@ -269,9 +270,21 @@ namespace Umbraco.Core return _umbracoApplicationUrl; } } + + /// + /// Resets the url. + /// + public void ResetUmbracoApplicationUrl() + { + _umbracoApplicationUrl = null; + } // ReSharper disable once InconsistentNaming - internal string _umbracoApplicationUrl; + internal string _umbracoApplicationUrl; + + internal List _umbracoApplicationDomains = new List(); + + internal string _umbracoApplicationDeploymentId; private Lazy _configured; internal MainDom MainDom { get; private set; } diff --git a/src/Umbraco.Core/Events/ScopeEventDispatcherBase.cs b/src/Umbraco.Core/Events/ScopeEventDispatcherBase.cs index c703a10cb4..7115f83eb6 100644 --- a/src/Umbraco.Core/Events/ScopeEventDispatcherBase.cs +++ b/src/Umbraco.Core/Events/ScopeEventDispatcherBase.cs @@ -76,260 +76,257 @@ namespace Umbraco.Core.Events { if (_events == null) return Enumerable.Empty(); - + + IReadOnlyList events; switch (filter) { case EventDefinitionFilter.All: - return FilterSupersededAndUpdateToLatestEntity(_events); + events = _events; + break; case EventDefinitionFilter.FirstIn: var l1 = new OrderedHashSet(); foreach (var e in _events) - { l1.Add(e); - } - return FilterSupersededAndUpdateToLatestEntity(l1); + events = l1; + break; case EventDefinitionFilter.LastIn: var l2 = new OrderedHashSet(keepOldest: false); foreach (var e in _events) - { l2.Add(e); - } - return FilterSupersededAndUpdateToLatestEntity(l2); + events = l2; + break; default: throw new ArgumentOutOfRangeException("filter", filter, null); } + + return FilterSupersededAndUpdateToLatestEntity(events); } - - private class EventDefinitionTypeData + + private class EventDefinitionInfos { public IEventDefinition EventDefinition { get; set; } - public Type EventArgType { get; set; } - public SupersedeEventAttribute[] SupersedeAttributes { get; set; } + public Type[] SupersedeTypes { get; set; } } - - /// - /// This will iterate over the events (latest first) and filter out any events or entities in event args that are included - /// in more recent events that Supersede previous ones. For example, If an Entity has been Saved and then Deleted, we don't want - /// to raise the Saved event (well actually we just don't want to include it in the args for that saved event) - /// - /// - /// - private static IEnumerable FilterSupersededAndUpdateToLatestEntity(IReadOnlyList events) + + // fixme + // this is way too convoluted, the superceede attribute is used only on DeleteEventargs to specify + // that it superceeds save, publish, move and copy - BUT - publish event args is also used for + // unpublishing and should NOT be superceeded - so really it should not be managed at event args + // level but at event level + // + // what we want is: + // if an entity is deleted, then all Saved, Moved, Copied, Published events prior to this should + // not trigger for the entity - and even though, does it make any sense? making a copy of an entity + // should ... trigger? + // + // not going to refactor it all - we probably want to *always* trigger event but tell people that + // due to scopes, they should not expected eg a saved entity to still be around - however, now, + // going to write a ugly condition to deal with U4-10764 + + // iterates over the events (latest first) and filter out any events or entities in event args that are included + // in more recent events that Supersede previous ones. For example, If an Entity has been Saved and then Deleted, we don't want + // to raise the Saved event (well actually we just don't want to include it in the args for that saved event) + internal static IEnumerable FilterSupersededAndUpdateToLatestEntity(IReadOnlyList events) { - //used to keep the 'latest' entity and associated event definition data - var allEntities = new List>(); - - //tracks all CancellableObjectEventArgs instances in the events which is the only type of args we can work with - var cancelableArgs = new List(); + // keeps the 'latest' entity and associated event data + var entities = new List>(); + // collects the event definitions + // collects the arguments in result, that require their entities to be updated var result = new List(); + var resultArgs = new List(); - //This will eagerly load all of the event arg types and their attributes so we don't have to continuously look this data up - var allArgTypesWithAttributes = events.Select(x => x.Args.GetType()) + // eagerly fetch superceeded arg types for each arg type + var argTypeSuperceeding = events.Select(x => x.Args.GetType()) .Distinct() - .ToDictionary(x => x, x => x.GetCustomAttributes(false).ToArray()); - - //Iterate all events and collect the actual entities in them and relates them to their corresponding EventDefinitionTypeData - //we'll process the list in reverse because events are added in the order they are raised and we want to filter out - //any entities from event args that are not longer relevant - //(i.e. if an item is Deleted after it's Saved, we won't include the item in the Saved args) + .ToDictionary(x => x, x => x.GetCustomAttributes(false).Select(y => y.SupersededEventArgsType).ToArray()); + + // iterate over all events and filter + // + // process the list in reverse, because events are added in the order they are raised and we want to keep + // the latest (most recent) entities and filter out what is not relevant anymore (too old), eg if an entity + // is Deleted after being Saved, we want to filter out the Saved event for (var index = events.Count - 1; index >= 0; index--) { - var eventDefinition = events[index]; + var def = events[index]; - var argType = eventDefinition.Args.GetType(); - var attributes = allArgTypesWithAttributes[eventDefinition.Args.GetType()]; - - var meta = new EventDefinitionTypeData + var infos = new EventDefinitionInfos { - EventDefinition = eventDefinition, - EventArgType = argType, - SupersedeAttributes = attributes + EventDefinition = def, + SupersedeTypes = argTypeSuperceeding[def.Args.GetType()] }; - var args = eventDefinition.Args as CancellableObjectEventArgs; - if (args != null) + var args = def.Args as CancellableObjectEventArgs; + if (args == null) { - var list = TypeHelper.CreateGenericEnumerableFromObject(args.EventObject); - - if (list == null) + // not a cancellable event arg, include event definition in result + result.Add(def); + } + else + { + // event object can either be a single object or an enumerable of objects + // try to get as an enumerable, get null if it's not + var eventObjects = TypeHelper.CreateGenericEnumerableFromObject(args.EventObject); + if (eventObjects == null) { - //extract the event object - var obj = args.EventObject as IEntity; - if (obj != null) + // single object, cast as an IEntity + // if cannot cast, cannot filter, nothing - just include event definition in result + var eventEntity = args.EventObject as IEntity; + if (eventEntity == null) { - //Now check if this entity already exists in other event args that supersede this current event arg type - if (IsFiltered(obj, meta, allEntities) == false) - { - //if it's not filtered we can adde these args to the response - cancelableArgs.Add(args); - result.Add(eventDefinition); - //track the entity - allEntities.Add(Tuple.Create(obj, meta)); - } + result.Add(def); + continue; } - else + + // look for this entity in superceding event args + // found = must be removed (ie not added), else track + if (IsSuperceeded(eventEntity, infos, entities) == false) { - //Can't retrieve the entity so cant' filter or inspect, just add to the output - result.Add(eventDefinition); + // track + entities.Add(Tuple.Create(eventEntity, infos)); + + // track result arguments + // include event definition in result + resultArgs.Add(args); + result.Add(def); } } else { + // enumerable of objects var toRemove = new List(); - foreach (var entity in list) + foreach (var eventObject in eventObjects) { - //extract the event object - var obj = entity as IEntity; - if (obj != null) - { - //Now check if this entity already exists in other event args that supersede this current event arg type - if (IsFiltered(obj, meta, allEntities)) - { - //track it to be removed - toRemove.Add(obj); - } - else - { - //track the entity, it's not filtered - allEntities.Add(Tuple.Create(obj, meta)); - } - } + // extract the event object, cast as an IEntity + // if cannot cast, cannot filter, nothing to do - just leave it in the list & continue + var eventEntity = eventObject as IEntity; + if (eventEntity == null) + continue; + + // look for this entity in superceding event args + // found = must be removed, else track + if (IsSuperceeded(eventEntity, infos, entities)) + toRemove.Add(eventEntity); else - { - //we don't need to do anything here, we can't cast to IEntity so we cannot filter, so it will just remain in the list - } + entities.Add(Tuple.Create(eventEntity, infos)); } - //remove anything that has been filtered + // remove superceded entities foreach (var entity in toRemove) - { - list.Remove(entity); - } + eventObjects.Remove(entity); - //track the event and include in the response if there's still entities remaining in the list - if (list.Count > 0) + // if there are still entities in the list, keep the event definition + if (eventObjects.Count > 0) { if (toRemove.Count > 0) { - //re-assign if the items have changed - args.EventObject = list; + // re-assign if changed + args.EventObject = eventObjects; } - cancelableArgs.Add(args); - result.Add(eventDefinition); + + // track result arguments + // include event definition in result + resultArgs.Add(args); + result.Add(def); } } } - else - { - //it's not a cancelable event arg so we just include it in the result - result.Add(eventDefinition); - } } - //Now we'll deal with ensuring that only the latest(non stale) entities are used throughout all event args - UpdateToLatestEntities(allEntities, cancelableArgs); + // go over all args in result, and update them with the latest instanceof each entity + UpdateToLatestEntities(entities, resultArgs); - //we need to reverse the result since we've been adding by latest added events first! + // reverse, since we processed the list in reverse result.Reverse(); return result; } - private static void UpdateToLatestEntities(IEnumerable> allEntities, IEnumerable cancelableArgs) + // edits event args to use the latest instance of each entity + private static void UpdateToLatestEntities(IEnumerable> entities, IEnumerable args) { - //Now we'll deal with ensuring that only the latest(non stale) entities are used throughout all event args - + // get the latest entities + // ordered hash set + keepOldest will keep the latest inserted entity (in case of duplicates) var latestEntities = new OrderedHashSet(keepOldest: true); - foreach (var entity in allEntities.OrderByDescending(entity => entity.Item1.UpdateDate)) - { + foreach (var entity in entities.OrderByDescending(entity => entity.Item1.UpdateDate)) latestEntities.Add(entity.Item1); - } - foreach (var args in cancelableArgs) + foreach (var arg in args) { - var list = TypeHelper.CreateGenericEnumerableFromObject(args.EventObject); - if (list == null) + // event object can either be a single object or an enumerable of objects + // try to get as an enumerable, get null if it's not + var eventObjects = TypeHelper.CreateGenericEnumerableFromObject(arg.EventObject); + if (eventObjects == null) { - //try to find the args entity in the latest entity - based on the equality operators, this will - //match by Id since that is the default equality checker for IEntity. If one is found, than it is - //the most recent entity instance so update the args with that instance so we don't emit a stale instance. - var foundEntity = latestEntities.FirstOrDefault(x => Equals(x, args.EventObject)); + // single object + // look for a more recent entity for that object, and replace if any + // works by "equalling" entities ie the more recent one "equals" this one (though different object) + var foundEntity = latestEntities.FirstOrDefault(x => Equals(x, arg.EventObject)); if (foundEntity != null) - { - args.EventObject = foundEntity; - } + arg.EventObject = foundEntity; } else { + // enumerable of objects + // same as above but for each object var updated = false; - - for (int i = 0; i < list.Count; i++) + for (var i = 0; i < eventObjects.Count; i++) { - //try to find the args entity in the latest entity - based on the equality operators, this will - //match by Id since that is the default equality checker for IEntity. If one is found, than it is - //the most recent entity instance so update the args with that instance so we don't emit a stale instance. - var foundEntity = latestEntities.FirstOrDefault(x => Equals(x, list[i])); - if (foundEntity != null) - { - list[i] = foundEntity; - updated = true; - } + var foundEntity = latestEntities.FirstOrDefault(x => Equals(x, eventObjects[i])); + if (foundEntity == null) continue; + eventObjects[i] = foundEntity; + updated = true; } if (updated) - { - args.EventObject = list; - } + arg.EventObject = eventObjects; } } } - /// - /// This will check against all of the processed entity/events (allEntities) to see if this entity already exists in - /// event args that supersede the event args being passed in and if so returns true. - /// - /// - /// - /// - /// - private static bool IsFiltered( - IEntity entity, - EventDefinitionTypeData eventDef, - List> allEntities) + // determines if a given entity, appearing in a given event definition, should be filtered out, + // considering the entities that have already been visited - an entity is filtered out if it + // appears in another even definition, which superceedes this event definition. + private static bool IsSuperceeded(IEntity entity, EventDefinitionInfos infos, List> entities) { - var argType = eventDef.EventDefinition.Args.GetType(); + //var argType = meta.EventArgsType; + var argType = infos.EventDefinition.Args.GetType(); - //check if the entity is found in any processed event data that could possible supersede this one - var foundByEntity = allEntities - .Where(x => x.Item2.SupersedeAttributes.Length > 0 - //if it's the same arg type than it cannot supersede - && x.Item2.EventArgType != argType - && Equals(x.Item1, entity)) + // look for other instances of the same entity, coming from an event args that supercedes other event args, + // ie is marked with the attribute, and is not this event args (cannot supersede itself) + var superceeding = entities + .Where(x => x.Item2.SupersedeTypes.Length > 0 // has the attribute + && x.Item2.EventDefinition.Args.GetType() != argType // is not the same + && Equals(x.Item1, entity)) // same entity .ToArray(); - //no args have been processed with this entity so it should not be filtered - if (foundByEntity.Length == 0) + // first time we see this entity = not filtered + if (superceeding.Length == 0) return false; + // fixme see notes above + // delete event args does NOT superceedes 'unpublished' event + if (argType.IsGenericType && argType.GetGenericTypeDefinition() == typeof(PublishEventArgs<>) && infos.EventDefinition.EventName == "UnPublished") + return false; + + // found occurences, need to determine if this event args is superceded if (argType.IsGenericType) { - var supercededBy = foundByEntity - .FirstOrDefault(x => - x.Item2.SupersedeAttributes.Any(y => - //if the attribute type is a generic type def then compare with the generic type def of the event arg - (y.SupersededEventArgsType.IsGenericTypeDefinition && y.SupersededEventArgsType == argType.GetGenericTypeDefinition()) - //if the attribute type is not a generic type def then compare with the normal type of the event arg - || (y.SupersededEventArgsType.IsGenericTypeDefinition == false && y.SupersededEventArgsType == argType))); + // generic, must compare type arguments + var supercededBy = superceeding.FirstOrDefault(x => + x.Item2.SupersedeTypes.Any(y => + // superceeding a generic type which has the same generic type definition + // fixme no matter the generic type parameters? could be different? + y.IsGenericTypeDefinition && y == argType.GetGenericTypeDefinition() + // or superceeding a non-generic type which is ... fixme how is this ever possible? argType *is* generic? + || y.IsGenericTypeDefinition == false && y == argType)); return supercededBy != null; } else { - var supercededBy = foundByEntity - .FirstOrDefault(x => - x.Item2.SupersedeAttributes.Any(y => - //since the event arg type is not a generic type, then we just compare type 1:1 - y.SupersededEventArgsType == argType)); + // non-generic, can compare types 1:1 + var supercededBy = superceeding.FirstOrDefault(x => + x.Item2.SupersedeTypes.Any(y => y == argType)); return supercededBy != null; } } diff --git a/src/Umbraco.Core/Sync/ApplicationUrlHelper.cs b/src/Umbraco.Core/Sync/ApplicationUrlHelper.cs index 7b713374aa..5229ce7dec 100644 --- a/src/Umbraco.Core/Sync/ApplicationUrlHelper.cs +++ b/src/Umbraco.Core/Sync/ApplicationUrlHelper.cs @@ -3,6 +3,7 @@ using System.Web; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; +using Umbraco.Core.Logging; using Umbraco.Core.ObjectResolution; namespace Umbraco.Core.Sync @@ -46,8 +47,20 @@ namespace Umbraco.Core.Sync // settings: for unit tests only internal static void EnsureApplicationUrl(ApplicationContext appContext, HttpRequestBase request = null, IUmbracoSettingsSection settings = null) { + bool newApplicationUrl = false; + if (request != null) + { + var applicationUrl = GetApplicationUrlFromRequest(request); + newApplicationUrl = !appContext._umbracoApplicationDomains.Contains(applicationUrl); + if (newApplicationUrl) + { + appContext._umbracoApplicationDomains.Add(applicationUrl); + LogHelper.Info(typeof(ApplicationUrlHelper), $"New ApplicationUrl detected: {applicationUrl}"); + } + } + // if initialized, return - if (appContext._umbracoApplicationUrl != null) return; + if (appContext._umbracoApplicationUrl != null && !newApplicationUrl) return; var logger = appContext.ProfilingLogger.Logger; @@ -132,6 +145,12 @@ namespace Umbraco.Core.Sync { var logger = appContext.ProfilingLogger.Logger; + appContext._umbracoApplicationUrl = GetApplicationUrlFromRequest(request); + logger.Info(TypeOfApplicationUrlHelper, "ApplicationUrl: " + appContext.UmbracoApplicationUrl + " (UmbracoModule request)"); + } + + private static string GetApplicationUrlFromRequest(HttpRequestBase request) + { // if (HTTP and SSL not required) or (HTTPS and SSL required), // use ports from request // otherwise, @@ -147,8 +166,7 @@ namespace Umbraco.Core.Sync var ssl = useSsl ? "s" : ""; // force, whatever the first request var url = "http" + ssl + "://" + request.ServerVariables["SERVER_NAME"] + port + IOHelper.ResolveUrl(SystemDirectories.Umbraco); - appContext._umbracoApplicationUrl = url.TrimEnd('/'); - logger.Info(TypeOfApplicationUrlHelper, "ApplicationUrl: " + appContext.UmbracoApplicationUrl + " (UmbracoModule request)"); + return url.TrimEnd('/'); } } } \ No newline at end of file diff --git a/src/Umbraco.Tests/Scoping/ScopeEventDispatcherTests.cs b/src/Umbraco.Tests/Scoping/ScopeEventDispatcherTests.cs index 825432756b..3c8f4b3422 100644 --- a/src/Umbraco.Tests/Scoping/ScopeEventDispatcherTests.cs +++ b/src/Umbraco.Tests/Scoping/ScopeEventDispatcherTests.cs @@ -3,10 +3,13 @@ using System.Collections.Generic; using System.Linq; using Moq; using NUnit.Framework; +using umbraco.cms.businesslogic; +using Umbraco.Core; using Umbraco.Core.Events; using Umbraco.Core.Models; using Umbraco.Core.Persistence; using Umbraco.Core.Scoping; +using Umbraco.Core.Services; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; @@ -99,7 +102,7 @@ namespace Umbraco.Tests.Scoping DoDeleteForContent += OnDoThingFail; DoForTestArgs += OnDoThingFail; DoForTestArgs2 += OnDoThingFail; - + var contentType = MockedContentTypes.CreateBasicContentType(); var content1 = MockedContent.CreateBasicContent(contentType); @@ -114,10 +117,10 @@ namespace Umbraco.Tests.Scoping var scopeProvider = new ScopeProvider(Mock.Of()); using (var scope = scopeProvider.CreateScope(eventDispatcher: new PassiveEventDispatcher())) { - + //content1 will be filtered from the args scope.Events.Dispatch(DoSaveForContent, this, new SaveEventArgs(new[]{ content1 , content3})); - scope.Events.Dispatch(DoDeleteForContent, this, new DeleteEventArgs(content1)); + scope.Events.Dispatch(DoDeleteForContent, this, new DeleteEventArgs(content1), "DoDeleteForContent"); scope.Events.Dispatch(DoSaveForContent, this, new SaveEventArgs(content2)); //this entire event will be filtered scope.Events.Dispatch(DoForTestArgs, this, new TestEventArgs(content1)); @@ -141,15 +144,36 @@ namespace Umbraco.Tests.Scoping } } + [Test] + public void SupersededEvents2() + { + var contentService = Mock.Of(); + var content = Mock.Of(); + var l1 = new List + { + new EventDefinition>(Test_UnPublished, contentService, new PublishEventArgs(new [] { content }), "UnPublished"), + new EventDefinition>(Test_Deleted, contentService, new DeleteEventArgs(new [] { content }), "Deleted") + }; + + var l2 = new OrderedHashSet(keepOldest: false); + foreach (var e in l1) + l2.Add(e); + + var l3 = ScopeEventDispatcherBase.FilterSupersededAndUpdateToLatestEntity(l2); + + // see U4-10764 + Assert.AreEqual(2, l3.Count()); + } + /// /// This will test that when we track events that before we Get the events we normalize all of the - /// event entities to be the latest one (most current) found amongst the event so that there is + /// event entities to be the latest one (most current) found amongst the event so that there is /// no 'stale' entities in any of the args /// [Test] public void LatestEntities() { - DoSaveForContent += OnDoThingFail; + DoSaveForContent += OnDoThingFail; var now = DateTime.Now; var contentType = MockedContentTypes.CreateBasicContentType(); @@ -165,7 +189,7 @@ namespace Umbraco.Tests.Scoping var scopeProvider = new ScopeProvider(Mock.Of()); using (var scope = scopeProvider.CreateScope(eventDispatcher: new PassiveEventDispatcher())) - { + { scope.Events.Dispatch(DoSaveForContent, this, new SaveEventArgs(content1)); scope.Events.Dispatch(DoSaveForContent, this, new SaveEventArgs(content2)); scope.Events.Dispatch(DoSaveForContent, this, new SaveEventArgs(content3)); @@ -173,7 +197,7 @@ namespace Umbraco.Tests.Scoping // events have been queued var events = scope.Events.GetEvents(EventDefinitionFilter.All).ToArray(); Assert.AreEqual(3, events.Length); - + foreach (var t in events) { var args = (SaveEventArgs)t.Args; @@ -212,7 +236,7 @@ namespace Umbraco.Tests.Scoping // events have been queued var events = scope.Events.GetEvents(EventDefinitionFilter.FirstIn).ToArray(); - Assert.AreEqual(1, events.Length); + Assert.AreEqual(1, events.Length); Assert.AreEqual(content1, ((SaveEventArgs) events[0].Args).SavedEntities.First()); Assert.IsTrue(object.ReferenceEquals(content1, ((SaveEventArgs)events[0].Args).SavedEntities.First())); Assert.AreEqual(content1.UpdateDate, ((SaveEventArgs) events[0].Args).SavedEntities.First().UpdateDate); @@ -350,6 +374,9 @@ namespace Umbraco.Tests.Scoping public static event TypedEventHandler> DoThing3; + public static event TypedEventHandler> Test_UnPublished; + public static event TypedEventHandler> Test_Deleted; + public class TestEventArgs : CancellableObjectEventArgs { public TestEventArgs(object eventObject) : base(eventObject) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnodepreview.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnodepreview.directive.js index d98246ac42..556019857b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnodepreview.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnodepreview.directive.js @@ -77,7 +77,7 @@ @param {string} icon (binding): The node icon. @param {string} name (binding): The node name. -@param {boolean} published (binding): The node pusblished state. +@param {boolean} published (binding): The node published state. @param {string} description (binding): A short description. @param {boolean} sortable (binding): Will add a move cursor on the node preview. Can used in combination with ui-sortable. @param {boolean} allowRemove (binding): Show/Hide the remove button. diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbpasswordtoggle.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbpasswordtoggle.directive.js index d75b9e2de0..aac1b8dac1 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbpasswordtoggle.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbpasswordtoggle.directive.js @@ -1,4 +1,15 @@ -(function () { +/** +@ngdoc directive +@name umbraco.directives.directive:umbPasswordToggle +@restrict E +@scope + +@description +Added in Umbraco v. 7.7.4: Use this directive to render a password toggle. + +**/ + +(function () { 'use strict'; // comes from https://codepen.io/jakob-e/pen/eNBQaP diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbtable.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbtable.directive.js index 17d4dd93ff..c45a9f78e5 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbtable.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbtable.directive.js @@ -1,3 +1,114 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbTable +@restrict E +@scope + +@description +Added in Umbraco v. 7.4: Use this directive to render a data table. + +

Markup example

+
+    
+ + + + +
+
+ +

Controller example

+
+    (function () {
+        "use strict";
+    
+        function Controller() {
+    
+            var vm = this;
+    
+            vm.items = [
+                {
+                    "icon": "icon-document",
+                    "name": "My node 1",
+                    "published": true,
+                    "description": "A short description of my node",
+                    "author": "Author 1"
+                },
+                {
+                    "icon": "icon-document",
+                    "name": "My node 2",
+                    "published": true,
+                    "description": "A short description of my node",
+                    "author": "Author 2"
+                }
+            ];
+
+            vm.options = {
+                includeProperties: [
+                    { alias: "description", header: "Description" },
+                    { alias: "author", header: "Author" }
+                ]
+            };
+    
+            vm.selectItem = selectItem;
+            vm.clickItem = clickItem;
+            vm.selectAll = selectAll;
+            vm.isSelectedAll = isSelectedAll;
+            vm.isSortDirection = isSortDirection;
+            vm.sort = sort;
+
+            function selectAll($event) {
+                alert("select all");
+            }
+
+            function isSelectedAll() {
+                
+            }
+    
+            function clickItem(item) {
+                alert("click node");
+            }
+
+            function selectItem(selectedItem, $index, $event) {
+                alert("select node");
+            }
+            
+            function isSortDirection(col, direction) {
+                
+            }
+            
+            function sort(field, allow, isSystem) {
+                
+            }
+    
+        }
+    
+        angular.module("umbraco").controller("My.TableController", Controller);
+    
+    })();
+
+ +@param {string} icon (binding): The node icon. +@param {string} name (binding): The node name. +@param {string} published (binding): The node published state. +@param {function} onSelect (expression): Callback function when the row is selected. +@param {function} onClick (expression): Callback function when the "Name" column link is clicked. +@param {function} onSelectAll (expression): Callback function when selecting all items. +@param {function} onSelectedAll (expression): Callback function when all items are selected. +@param {function} onSortingDirection (expression): Callback function when sorting direction is changed. +@param {function} onSort (expression): Callback function when sorting items. +**/ + (function () { 'use strict'; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less index 2e5d1ef77d..b26ca2aede 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less @@ -535,7 +535,7 @@ color: @gray-3; } -.umb-grid .umb-cell-rte textarea { +.umb-grid .umb-cell-rte textarea.mceNoEditor { display: none !important; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-node-preview.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-node-preview.less index b2927c7ddc..9e038bd571 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-node-preview.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-node-preview.less @@ -36,6 +36,7 @@ .umb-node-preview__content { flex: 1 1 auto; margin-right: 25px; + overflow: hidden; } .umb-node-preview__name { @@ -49,6 +50,13 @@ color: @gray-3; } +.umb-node-preview__name, +.umb-node-preview__description { + /*text-overflow: ellipsis; + overflow: hidden;*/ + word-wrap: break-word; +} + .umb-node-preview__actions { flex: 0 0 auto; display: flex; diff --git a/src/Umbraco.Web.UI.Client/src/less/property-editors.less b/src/Umbraco.Web.UI.Client/src/less/property-editors.less index ffaa1a6a92..7082c15a14 100644 --- a/src/Umbraco.Web.UI.Client/src/less/property-editors.less +++ b/src/Umbraco.Web.UI.Client/src/less/property-editors.less @@ -281,6 +281,12 @@ ul.color-picker li a { display: block; } +.umb-sortable-thumbnails .umb-sortable-thumbnails__wrapper { + width: 120px; + height: 114px; + overflow: hidden; +} + .umb-sortable-thumbnails .umb-sortable-thumbnails__actions { position: absolute; bottom: 10px; @@ -308,6 +314,7 @@ ul.color-picker li a { justify-content: center; align-items: center; margin-left: 5px; + text-decoration: none; } .umb-sortable-thumbnails .umb-sortable-thumbnails__action.-red { diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/imagepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/imagepicker.controller.js index 48bb1e81ec..6e7071a7e4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/imagepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/imagepicker.controller.js @@ -1,20 +1,25 @@ function imageFilePickerController($scope) { - $scope.pick = function() { - $scope.mediaPickerDialog = {}; - $scope.mediaPickerDialog.view = "mediapicker"; - $scope.mediaPickerDialog.show = true; - - $scope.mediaPickerDialog.submit = function(model) { - $scope.model.value = model.selectedImages[0].image; - $scope.mediaPickerDialog.show = false; - $scope.mediaPickerDialog = null; + $scope.add = function() { + $scope.mediaPickerOverlay = { + view: "mediapicker", + disableFolderSelect: true, + onlyImages: true, + show: true, + submit: function (model) { + $scope.model.value = model.selectedImages[0].image; + $scope.mediaPickerOverlay.show = false; + $scope.mediaPickerOverlay = null; + }, + close: function () { + $scope.mediaPickerOverlay.show = false; + $scope.mediaPickerOverlay = null; + } }; + }; - $scope.mediaPickerDialog.close = function(oldModel) { - $scope.mediaPickerDialog.show = false; - $scope.mediaPickerDialog = null; - }; + $scope.remove = function () { + $scope.model.value = null; }; } diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/imagepicker.html b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/imagepicker.html index d9d988d7e5..1a37a624d5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/imagepicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/imagepicker.html @@ -1,19 +1,28 @@
-
    -
  • - - + + - + diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js index a282334cb5..747c5e5d72 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js @@ -640,16 +640,16 @@ angular.module("umbraco") var clear = true; //settings indicator shortcut - if ( ($scope.model.config.items.config && $scope.model.config.items.config.length > 0) || ($scope.model.config.items.styles && $scope.model.config.items.styles.length > 0)) { + if (($scope.model.config.items.config && $scope.model.config.items.config.length > 0) || ($scope.model.config.items.styles && $scope.model.config.items.styles.length > 0)) { $scope.hasSettings = true; } //ensure the grid has a column value set, //if nothing is found, set it to 12 - if ($scope.model.config.items.columns && angular.isString($scope.model.config.items.columns)) { - $scope.model.config.items.columns = parseInt($scope.model.config.items.columns); - } else { + if (!$scope.model.config.items.columns){ $scope.model.config.items.columns = 12; + } else if (angular.isString($scope.model.config.items.columns)) { + $scope.model.config.items.columns = parseInt($scope.model.config.items.columns); } if ($scope.model.value && $scope.model.value.sections && $scope.model.value.sections.length > 0 && $scope.model.value.sections[0].rows && $scope.model.value.sections[0].rows.length > 0) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js index 91777bfc3e..1874a0623e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js @@ -19,6 +19,8 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl $scope.images = []; $scope.ids = []; + $scope.isMultiPicker = multiPicker; + if ($scope.model.value) { var ids = $scope.model.value.split(','); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html index 8d33600594..99b1e60af3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html @@ -2,10 +2,10 @@

    - +
      -
    • +
    • @@ -29,11 +29,12 @@
  • - +
  • + + + +
- - -
Email Fout Vind - Hogte + Hoogte Help Icoon Import diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/ru.xml b/src/Umbraco.Web.UI/umbraco/config/lang/ru.xml index ec24bd69cb..115a227948 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/ru.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/ru.xml @@ -153,6 +153,7 @@ Нумерованный список Вставить макрос Вставить изображение + Повторить Править связи Вернуться к списку Сохранить @@ -168,6 +169,7 @@ Выбрать стиль Показать стили Вставить таблицу + Отменить Чтобы сменить тип документа для выбранного узла, сначала выберите тип из списка разрешенных для данного расположения. @@ -375,6 +377,7 @@ Свойства макроса Этот макрос не имеет редактируемых свойств Заголовок ссылки + Ни одной пиктограммы не найдено Вставить Изменить разрешения для Установить разрешения для @@ -596,6 +599,7 @@ Новый След Нет + Здесь пока нет элементов из Выкл Ok @@ -696,6 +700,8 @@ Выбрать дополнительно Выбрать по-умолчанию добавлены + Оставьте пустым или задайте 0 для снятия лимита + Максимальное количество Страница @@ -1049,7 +1055,7 @@ - и указав на нужный файл. Пакеты Umbraco обычно являются архивами с расширением ".umb" или ".zip". + и указав на нужный файл. Пакеты Umbraco обычно являются архивами с расширением '.zip'. ]]> Перетащите сюда или нажмите здесь для выбора файла пакета @@ -1215,12 +1221,12 @@ Заголовок - Укажите заголовок + Укажите заголовок ссылки выбрать страницу сайта указать внешнюю ссылку Укажите ссылку Ссылка - В новом окне + Открыть в новом окне Переименована @@ -1393,8 +1399,6 @@ '%0%' сейчас заблокирован При блокировке пользователя произошла ошибка Группы пользователей установлены - Удалено %0% групп пользователей - '%0%' была удалена Разблокировано %0% пользователей При разблокировке пользователей произошла ошибка '%0%' сейчас разблокирован @@ -1517,8 +1521,8 @@ Поле замены Добавить значение по-умолчанию Значение по-умолчанию - Альтернативное поле - Текст по-умолчанию + Поле замены + Значение по-умолчанию Регистр Выбрать поле Преобразовать переводы строк @@ -1715,11 +1719,22 @@ Профиль Сбросить пароль Поиск всех дочерних документов + Выбрать группы пользователей Отправить приглашение Сессия истекает через Разделы, доступные пользователю Начальный узел не задан Начальные узлы не заданы + Имя (А-Я) + Имя (Я-А) + Сначала новые + Сначала старые + Недавно зашедшие + Активные + Все + Отключенные + Заблокированные + Приглашенные Начальный узел содержимого Можно ограничить доступ к дереву содержимого (какой-либо его части), задав начальный узел Начальные узлы содержимого @@ -1736,6 +1751,7 @@ Новому пользователю было отправлено приглашение, в котором содержатся инструкции для входа в панель Umbraco. Здравствуйте и добро пожаловать в Umbraco! Все будет готово в течении пары минут, нам лишь нужно задать Ваш пароль для входа и добавить аватар. Загрузите изображение, это поможет другим пользователям идентифицировать Вас. + Управление пользователями Разрешения для пользователя Автор Ваша недавняя история diff --git a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs index 27fbc6d5c5..b1b013c974 100644 --- a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs +++ b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs @@ -805,13 +805,13 @@ namespace Umbraco.Web.Cache /// /// Used to cache all found event handlers /// - private static readonly ConcurrentDictionary FoundHandlers = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary FoundHandlers = new ConcurrentDictionary(); internal static MethodInfo FindHandler(IEventDefinition eventDefinition) { var name = eventDefinition.Sender.GetType().Name + "_" + eventDefinition.EventName; - return FoundHandlers.GetOrAdd(eventDefinition, _ => CandidateHandlers.Value.FirstOrDefault(x => x.Name == name)); + return FoundHandlers.GetOrAdd(name, n => CandidateHandlers.Value.FirstOrDefault(x => x.Name == n)); } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs b/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs index c6121b2e10..2fa919716c 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs @@ -58,7 +58,7 @@ namespace Umbraco.Web.Models.Mapping .ToDictionary(t => t.Alias, t => t.Name))) .ForMember(display => display.Tabs, expression => expression.ResolveUsing(new TabsAndPropertiesResolver(applicationContext.Services.TextService))) .ForMember(display => display.AllowedActions, expression => expression.ResolveUsing( - new ActionButtonsResolver(new Lazy(() => applicationContext.Services.UserService)))) + new ActionButtonsResolver(new Lazy(() => applicationContext.Services.UserService), new Lazy(() => applicationContext.Services.ContentService)))) .AfterMap((content, display) => AfterMap(content, display, applicationContext.Services.DataTypeService, applicationContext.Services.TextService, applicationContext.Services.ContentTypeService)); @@ -154,10 +154,14 @@ namespace Umbraco.Web.Models.Mapping private class ActionButtonsResolver : ValueResolver> { private readonly Lazy _userService; + private readonly Lazy _contentService; - public ActionButtonsResolver(Lazy userService) + public ActionButtonsResolver(Lazy userService, Lazy contentService) { + if (userService == null) throw new ArgumentNullException("userService"); + if (contentService == null) throw new ArgumentNullException("contentService"); _userService = userService; + _contentService = contentService; } protected override IEnumerable ResolveCore(IContent source) @@ -169,14 +173,21 @@ namespace Umbraco.Web.Models.Mapping } var svc = _userService.Value; - var permissions = svc.GetPermissions( + string path; + if (source.HasIdentity) + path = source.Path; + else + { + var parent = _contentService.Value.GetById(source.ParentId); + path = parent == null ? "-1" : parent.Path; + } + + var permissions = svc.GetPermissionsForPath( //TODO: This is certainly not ideal usage here - perhaps the best way to deal with this in the future is // with the IUmbracoContextAccessor. In the meantime, if used outside of a web app this will throw a null // refrence exception :( UmbracoContext.Current.Security.CurrentUser, - // Here we need to do a special check since this could be new content, in which case we need to get the permissions - // from the parent, not the existing one otherwise permissions would be coming from the root since Id is 0. - source.HasIdentity ? source.Id : source.ParentId) + path) .GetAllPermissions(); return permissions; diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs index a4793f9a67..fb40905a03 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs @@ -74,11 +74,47 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache public virtual IEnumerable GetAtRoot(UmbracoContext umbracoContext, bool preview) { - //TODO: We should be able to look these ids first in Examine! + var searchProvider = GetSearchProviderSafe(); + + if (searchProvider != null) + { + try + { + // first check in Examine for the cache values + // +(+parentID:-1) +__IndexType:media + + var criteria = searchProvider.CreateSearchCriteria("media"); + var filter = criteria.ParentId(-1).Not().Field(UmbracoContentIndexer.IndexPathFieldName, "-1,-21,".MultipleCharacterWildcard()); + + var result = searchProvider.Search(filter.Compile()); + if (result != null) + return result.Select(x => CreateFromCacheValues(ConvertFromSearchResult(x))); + } + catch (Exception ex) + { + if (ex is FileNotFoundException) + { + //Currently examine is throwing FileNotFound exceptions when we have a loadbalanced filestore and a node is published in umbraco + //See this thread: http://examine.cdodeplex.com/discussions/264341 + //Catch the exception here for the time being, and just fallback to GetMedia + //TODO: Need to fix examine in LB scenarios! + LogHelper.Error("Could not load data from Examine index for media", ex); + } + else if (ex is AlreadyClosedException) + { + //If the app domain is shutting down and the site is under heavy load the index reader will be closed and it really cannot + //be re-opened since the app domain is shutting down. In this case we have no option but to try to load the data from the db. + LogHelper.Error("Could not load data from Examine index for media, the app domain is most likely in a shutdown state", ex); + } + else throw; + } + } + + //something went wrong, fetch from the db var rootMedia = _applicationContext.Services.MediaService.GetRootMedia(); - return rootMedia.Select(m => GetUmbracoMedia(m.Id)); - } + return rootMedia.Select(m => CreateFromCacheValues(ConvertFromIMedia(m))); + } public virtual IPublishedContent GetSingleByXPath(UmbracoContext umbracoContext, bool preview, string xpath, XPathVariable[] vars) {