diff --git a/src/SQLCE4Umbraco/app.config b/src/SQLCE4Umbraco/app.config index fb4b506207..95a7e73c9e 100644 --- a/src/SQLCE4Umbraco/app.config +++ b/src/SQLCE4Umbraco/app.config @@ -16,19 +16,19 @@ - + - + - + - + diff --git a/src/Umbraco.Core/Models/Membership/IUser.cs b/src/Umbraco.Core/Models/Membership/IUser.cs index b7afdeb624..2a1d904ecc 100644 --- a/src/Umbraco.Core/Models/Membership/IUser.cs +++ b/src/Umbraco.Core/Models/Membership/IUser.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using Umbraco.Core.Models.EntityBase; namespace Umbraco.Core.Models.Membership @@ -16,7 +17,11 @@ namespace Umbraco.Core.Models.Membership int SessionTimeout { get; set; } int[] StartContentIds { get; set; } int[] StartMediaIds { get; set; } - string Language { get; set; } + string Language { get; set; } + + [Obsolete("This should not be used it exists for legacy reasons only, use user groups instead, it will be removed in future versions")] + [EditorBrowsable(EditorBrowsableState.Never)] + IUserType UserType { get; set; } DateTime? EmailConfirmedDate { get; set; } DateTime? InvitedDate { get; set; } @@ -32,6 +37,14 @@ namespace Umbraco.Core.Models.Membership IEnumerable AllowedSections { get; } + [Obsolete("This should not be used it exists for legacy reasons only, use user groups instead, it will be removed in future versions")] + [EditorBrowsable(EditorBrowsableState.Never)] + void RemoveAllowedSection(string sectionAlias); + + [Obsolete("This should not be used it exists for legacy reasons only, use user groups instead, it will be removed in future versions")] + [EditorBrowsable(EditorBrowsableState.Never)] + void AddAllowedSection(string sectionAlias); + /// /// Exposes the basic profile data /// diff --git a/src/Umbraco.Core/Models/Membership/IUserType.cs b/src/Umbraco.Core/Models/Membership/IUserType.cs new file mode 100644 index 0000000000..ba004cea4a --- /dev/null +++ b/src/Umbraco.Core/Models/Membership/IUserType.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using Umbraco.Core.Models.EntityBase; + +namespace Umbraco.Core.Models.Membership +{ + [Obsolete("This should not be used it exists for legacy reasons only, use user groups instead, it will be removed in future versions")] + [EditorBrowsable(EditorBrowsableState.Never)] + public interface IUserType : IAggregateRoot + { + string Alias { get; set; } + string Name { get; set; } + IEnumerable Permissions { get; set; } + + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Membership/User.cs b/src/Umbraco.Core/Models/Membership/User.cs index b64fd07452..aeb3148ceb 100644 --- a/src/Umbraco.Core/Models/Membership/User.cs +++ b/src/Umbraco.Core/Models/Membership/User.cs @@ -2,10 +2,12 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; +using System.ComponentModel; using System.Linq; using System.Reflection; using System.Runtime.Serialization; using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; using Umbraco.Core.Models.EntityBase; namespace Umbraco.Core.Models.Membership @@ -116,7 +118,8 @@ namespace Umbraco.Core.Models.Membership private DateTime _lastLoginDate; private DateTime _lastLockoutDate; private bool _defaultToLiveEditing; - private IDictionary _additionalData; + private IDictionary _additionalData; + private object _additionalDataLock = new object(); private static readonly Lazy Ps = new Lazy(); @@ -162,7 +165,7 @@ namespace Umbraco.Core.Models.Membership get { return Id; } set { throw new NotSupportedException("Cannot set the provider user key for a user"); } } - + [DataMember] public DateTime? EmailConfirmedDate { @@ -278,6 +281,165 @@ namespace Umbraco.Core.Models.Membership get { return _allowedSections ?? (_allowedSections = new List(_userGroups.SelectMany(x => x.AllowedSections).Distinct())); } } + [Obsolete("This should not be used it exists for legacy reasons only, use user groups instead, it will be removed in future versions")] + [EditorBrowsable(EditorBrowsableState.Never)] + IUserType IUser.UserType + { + get + { + //the best we can do here is to return the user's first user group as a IUserType object + //but we should attempt to return any group that is the built in ones first + var groups = Groups.ToArray(); + if (groups.Length == 0) + { + //In backwards compatibility land, a user type cannot be null! so we need to return a fake one. + return new UserType + { + Alias = "temp", + Id = int.MinValue, + Key = Guid.Empty, + CreateDate = default(DateTime), + DeletedDate = null, + Name = "Temp", + Permissions = new List(), + UpdateDate = default(DateTime) + }; + } + var builtIns = new[] { Constants.Security.AdminGroupAlias, "writer", "editor", "translator" }; + var foundBuiltIn = groups.FirstOrDefault(x => builtIns.Contains(x.Alias)); + IUserGroup realGroup; + if (foundBuiltIn != null) + { + //if the group isn't IUserGroup we'll need to look it up + realGroup = foundBuiltIn as IUserGroup ?? ApplicationContext.Current.Services.UserService.GetUserGroupById(foundBuiltIn.Id); + + //return a mapped version of the group + return new UserType + { + Alias = realGroup.Alias, + Id = realGroup.Id, + Key = realGroup.Key, + CreateDate = realGroup.CreateDate, + DeletedDate = realGroup.DeletedDate, + Name = realGroup.Name, + Permissions = realGroup.Permissions, + UpdateDate = realGroup.UpdateDate + }; + } + + //otherwise return the first + //if the group isn't IUserGroup we'll need to look it up + realGroup = groups[0] as IUserGroup ?? ApplicationContext.Current.Services.UserService.GetUserGroupById(groups[0].Id); + //return a mapped version of the group + return new UserType + { + Alias = realGroup.Alias, + Id = realGroup.Id, + Key = realGroup.Key, + CreateDate = realGroup.CreateDate, + DeletedDate = realGroup.DeletedDate, + Name = realGroup.Name, + Permissions = realGroup.Permissions, + UpdateDate = realGroup.UpdateDate + }; + } + set + { + //if old APIs are still using this lets first check if the user is part of the user group with the alias specified + if (Groups.Any(x => x.Alias == value.Alias)) + return; + + //the only other option we have here is to lookup the group (and we'll need to use singletons here :( ) + var found = ApplicationContext.Current.Services.UserService.GetUserGroupByAlias(value.Alias); + if (found == null) + throw new InvalidOperationException("No user group was found with the alias " + value.Alias + ", this API (IUser.UserType) is obsolete, use user groups instead"); + + //if it's found, all we can do is add it, we can't really replace them + AddGroup(found.ToReadOnlyGroup()); + } + } + + [Obsolete("This should not be used it exists for legacy reasons only, use user groups instead, it will be removed in future versions")] + [EditorBrowsable(EditorBrowsableState.Never)] + void IUser.RemoveAllowedSection(string sectionAlias) + { + //don't do anything if they aren't allowed it already + if (AllowedSections.Contains(sectionAlias) == false) + return; + + var groups = Groups.ToArray(); + //our only option here is to check if a custom group is created for this user, if so we can remove it from that group, otherwise we'll throw + //now we'll check if the user has a special 1:1 user group created for itself. This will occur if this method is used and also during an upgrade. + //this comes in the alias form of userName + 'Group' + var customUserGroup = groups.FirstOrDefault(x => x.Alias == (Username + "Group")); + if (customUserGroup != null) + { + //if the group isn't IUserGroup we'll need to look it up + var realGroup = customUserGroup as IUserGroup ?? ApplicationContext.Current.Services.UserService.GetUserGroupById(customUserGroup.Id); + realGroup.RemoveAllowedSection(sectionAlias); + //now we need to flag this for saving (hack!) + GroupsToSave.Add(realGroup); + } + else + { + throw new InvalidOperationException("Cannot remove the allowed section using this obsolete API. Modify the user's groups instead"); + } + + } + + [Obsolete("This should not be used it exists for legacy reasons only, use user groups instead, it will be removed in future versions")] + [EditorBrowsable(EditorBrowsableState.Never)] + void IUser.AddAllowedSection(string sectionAlias) + { + //don't do anything if they are allowed it already + if (AllowedSections.Contains(sectionAlias)) + return; + + //This is here for backwards compat only. + //First we'll check if the user is part of the 'admin' group. If so then we can ensure that the admin group has this section available to it. + //otherwise, the only thing we can do is create a custom user group for this user and add this section. + //We are checking for admin here because if the user is an admin and an allowed section is being added, then it's assumed it's to be added + //for the whole admin group (i.e. Forms installer does this for admins) + var groups = Groups.ToArray(); + var admin = groups.FirstOrDefault(x => x.Alias == Constants.Security.AdminGroupAlias); + if (admin != null) + { + //if the group isn't IUserGroup we'll need to look it up + var realGroup = admin as IUserGroup ?? ApplicationContext.Current.Services.UserService.GetUserGroupById(admin.Id); + realGroup.AddAllowedSection(sectionAlias); + //now we need to flag this for saving (hack!) + GroupsToSave.Add(realGroup); + } + + //now we'll check if the user has a special 1:1 user group created for itself. This will occur if this method is used and also during an upgrade. + //this comes in the alias form of userName + 'Group' + var customUserGroup = groups.FirstOrDefault(x => x.Alias == (Username + "Group")); + if (customUserGroup != null) + { + //if the group isn't IUserGroup we'll need to look it up + var realGroup = customUserGroup as IUserGroup ?? ApplicationContext.Current.Services.UserService.GetUserGroupById(customUserGroup.Id); + realGroup.AddAllowedSection(sectionAlias); + //now we need to flag this for saving (hack!) + GroupsToSave.Add(realGroup); + } + + //ok, so the user doesn't have a 1:1 group, we'll need to flag it for creation + var newUserGroup = new UserGroup + { + Alias = Username + "Group", + Name = "Group for " + Username + }; + newUserGroup.AddAllowedSection(sectionAlias); + GroupsToSave.Add(newUserGroup); + } + + /// + /// This used purely for hacking backwards compatibility into this class for < 7.7 compat + /// + [DoNotClone] + [IgnoreDataMember] + internal List GroupsToSave = new List(); + public IProfile ProfileData { get { return new WrappedUserProfile(this); } @@ -404,13 +566,23 @@ namespace Umbraco.Core.Models.Membership /// /// This is used as an internal cache for this entity - specifically for calculating start nodes so we don't re-calculated all of the time /// - [IgnoreDataMember] + [IgnoreDataMember] [DoNotClone] internal IDictionary AdditionalData { - get { return _additionalData ?? (_additionalData = new Dictionary()); } + get + { + lock (_additionalDataLock) + { + return _additionalData ?? (_additionalData = new Dictionary()); + } + } } + [IgnoreDataMember] + [DoNotClone] + internal object AdditionalDataLock { get { return _additionalDataLock; } } + public override object DeepClone() { var clone = (User)base.DeepClone(); @@ -419,15 +591,29 @@ namespace Umbraco.Core.Models.Membership //manually clone the start node props clone._startContentIds = _startContentIds.ToArray(); clone._startMediaIds = _startMediaIds.ToArray(); - //This ensures that any value in the dictionary that is deep cloneable is cloned too - foreach (var key in clone.AdditionalData.Keys.ToArray()) + + // this value has been cloned and points to the same object + // which obviously is bad - needs to point to a new object + clone._additionalDataLock = new object(); + + if (_additionalData != null) { - var deepCloneable = clone.AdditionalData[key] as IDeepCloneable; - if (deepCloneable != null) + // clone._additionalData points to the same dictionary, which is bad, because + // changing one clone impacts all of them - so we need to reset it with a fresh + // dictionary that will contain the same values - and, if some values are deep + // cloneable, they should be deep-cloned too + var cloneAdditionalData = clone._additionalData = new Dictionary(); + + lock (_additionalDataLock) { - clone.AdditionalData[key] = deepCloneable.DeepClone(); + foreach (var kvp in _additionalData) + { + var deepCloneable = kvp.Value as IDeepCloneable; + cloneAdditionalData[kvp.Key] = deepCloneable == null ? kvp.Value : deepCloneable.DeepClone(); + } } - } + } + //need to create new collections otherwise they'll get copied by ref clone._userGroups = new HashSet(_userGroups); clone._allowedSections = _allowedSections != null ? new List(_allowedSections) : null; diff --git a/src/Umbraco.Core/Models/Membership/UserType.cs b/src/Umbraco.Core/Models/Membership/UserType.cs new file mode 100644 index 0000000000..13865efed9 --- /dev/null +++ b/src/Umbraco.Core/Models/Membership/UserType.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Reflection; +using System.Runtime.Serialization; +using Umbraco.Core.Models.EntityBase; +using Umbraco.Core.Strings; + +namespace Umbraco.Core.Models.Membership +{ + [Obsolete("This should not be used it exists for legacy reasons only, use user groups instead, it will be removed in future versions")] + [EditorBrowsable(EditorBrowsableState.Never)] + [Serializable] + [DataContract(IsReference = true)] + internal class UserType : Entity, IUserType + { + private string _alias; + private string _name; + private IEnumerable _permissions; + private static readonly Lazy Ps = new Lazy(); + private class PropertySelectors + { + public readonly PropertyInfo NameSelector = ExpressionHelper.GetPropertyInfo(x => x.Name); + public readonly PropertyInfo AliasSelector = ExpressionHelper.GetPropertyInfo(x => x.Alias); + public readonly PropertyInfo PermissionsSelector = ExpressionHelper.GetPropertyInfo>(x => x.Permissions); + } + + [DataMember] + public string Alias + { + get { return _alias; } + set + { + SetPropertyValueAndDetectChanges( + value.ToCleanString(CleanStringType.Alias | CleanStringType.UmbracoCase), + ref _alias, + Ps.Value.AliasSelector); + } + } + + [DataMember] + public string Name + { + get { return _name; } + set { SetPropertyValueAndDetectChanges(value, ref _name, Ps.Value.NameSelector); } + } + + /// + /// The set of default permissions for the user type + /// + /// + /// By default each permission is simply a single char but we've made this an enumerable{string} to support a more flexible permissions structure in the future. + /// + [DataMember] + public IEnumerable Permissions + { + get { return _permissions; } + set + { + SetPropertyValueAndDetectChanges(value, ref _permissions, Ps.Value.PermissionsSelector, + //Custom comparer for enumerable + new DelegateEqualityComparer>( + (enum1, enum2) => enum1.UnsortedSequenceEqual(enum2), + enum1 => enum1.GetHashCode())); + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/UserExtensions.cs b/src/Umbraco.Core/Models/UserExtensions.cs index 061fd511b1..b087d6cee1 100644 --- a/src/Umbraco.Core/Models/UserExtensions.cs +++ b/src/Umbraco.Core/Models/UserExtensions.cs @@ -141,31 +141,23 @@ namespace Umbraco.Core.Models { if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Value cannot be null or whitespace.", "path"); - var formattedPath = "," + path + ","; - var formattedRecycleBinId = "," + recycleBinId.ToInvariantString() + ","; + // check for no access + if (startNodeIds.Length == 0) + return false; - //check for root path access - //TODO: This logic may change - if (startNodeIds.Length == 0 || startNodeIds.Contains(Constants.System.Root)) + // check for root access + if (startNodeIds.Contains(Constants.System.Root)) return true; - //only users with root access have access to the recycle bin so if the above check didn't pass than access is denied - if (formattedPath.Contains(formattedRecycleBinId)) - { + var formattedPath = "," + path + ","; + + // only users with root access have access to the recycle bin, + // if the above check didn't pass then access is denied + if (formattedPath.Contains("," + recycleBinId + ",")) return false; - } - //check for normal paths - foreach (var startNodeId in startNodeIds) - { - var formattedStartNodeId = "," + startNodeId.ToInvariantString() + ","; - - var hasAccess = formattedPath.Contains(formattedStartNodeId); - if (hasAccess) - return true; - } - - return false; + // check for a start node in the path + return startNodeIds.Any(x => formattedPath.Contains("," + x + ",")); } /// @@ -181,6 +173,7 @@ namespace Umbraco.Core.Models return user.Groups != null && user.Groups.Any(x => x.Alias == Constants.Security.AdminGroupAlias); } + // calc. start nodes, combining groups' and user's, and excluding what's in the bin public static int[] CalculateContentStartNodeIds(this IUser user, IEntityService entityService) { const string cacheKey = "AllContentStartNodes"; @@ -195,6 +188,7 @@ namespace Umbraco.Core.Models return vals; } + // calc. start nodes, combining groups' and user's, and excluding what's in the bin public static int[] CalculateMediaStartNodeIds(this IUser user, IEntityService entityService) { const string cacheKey = "AllMediaStartNodes"; @@ -212,22 +206,23 @@ namespace Umbraco.Core.Models private static int[] FromUserCache(IUser user, string cacheKey) { var entityUser = user as User; - if (entityUser != null) + if (entityUser == null) return null; + + lock (entityUser.AdditionalDataLock) { object allContentStartNodes; - if (entityUser.AdditionalData.TryGetValue(cacheKey, out allContentStartNodes)) - { - var asArray = allContentStartNodes as int[]; - if (asArray != null) return asArray; - } + return entityUser.AdditionalData.TryGetValue(cacheKey, out allContentStartNodes) + ? allContentStartNodes as int[] + : null; } - return null; } private static void ToUserCache(IUser user, string cacheKey, int[] vals) { var entityUser = user as User; - if (entityUser != null) + if (entityUser == null) return; + + lock (entityUser.AdditionalDataLock) { entityUser.AdditionalData[cacheKey] = vals; } @@ -238,7 +233,23 @@ namespace Umbraco.Core.Models return test.StartsWith(path) && test.Length > path.Length && test[path.Length] == ','; } - //TODO: Unit test this + private static string GetBinPath(UmbracoObjectTypes objectType) + { + var binPath = Constants.System.Root + ","; + switch (objectType) + { + case UmbracoObjectTypes.Document: + binPath += Constants.System.RecycleBinContent; + break; + case UmbracoObjectTypes.Media: + binPath += Constants.System.RecycleBinMedia; + break; + default: + throw new ArgumentOutOfRangeException("objectType"); + } + return binPath; + } + internal static int[] CombineStartNodes(UmbracoObjectTypes objectType, int[] groupSn, int[] userSn, IEntityService entityService) { // assume groupSn and userSn each don't contain duplicates @@ -246,13 +257,17 @@ namespace Umbraco.Core.Models var asn = groupSn.Concat(userSn).Distinct().ToArray(); var paths = entityService.GetAllPaths(objectType, asn).ToDictionary(x => x.Id, x => x.Path); - paths[-1] = "-1"; // entityService does not get that one + paths[Constants.System.Root] = Constants.System.Root.ToString(); // entityService does not get that one + + var binPath = GetBinPath(objectType); var lsn = new List(); foreach (var sn in groupSn) { string snp; - if (paths.TryGetValue(sn, out snp) == false) continue; // ignore + if (paths.TryGetValue(sn, out snp) == false) continue; // ignore rogue node (no path) + + if (StartsWithPath(snp, binPath)) continue; // ignore bin if (lsn.Any(x => StartsWithPath(snp, paths[x]))) continue; // skip if something above this sn lsn.RemoveAll(x => StartsWithPath(paths[x], snp)); // remove anything below this sn @@ -262,10 +277,12 @@ namespace Umbraco.Core.Models var usn = new List(); foreach (var sn in userSn) { - if (groupSn.Contains(sn)) continue; + if (groupSn.Contains(sn)) continue; // ignore, already there string snp; - if (paths.TryGetValue(sn, out snp) == false) continue; // ignore + if (paths.TryGetValue(sn, out snp) == false) continue; // ignore rogue node (no path) + + if (StartsWithPath(snp, binPath)) continue; // ignore bin if (usn.Any(x => StartsWithPath(paths[x], snp))) continue; // skip if something below this sn usn.RemoveAll(x => StartsWithPath(snp, paths[x])); // remove anything above this sn diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/AddUserGroupTables.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/AddUserGroupTables.cs index 127e1baca5..d1cf0a40d9 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/AddUserGroupTables.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/AddUserGroupTables.cs @@ -94,8 +94,18 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSevenZe WHERE u.id = 0"); // Rename some groups for consistency (plural form) - Execute.Sql("UPDATE umbracoUserGroup SET userGroupName = 'Writers' WHERE userGroupName = 'Writer'"); - Execute.Sql("UPDATE umbracoUserGroup SET userGroupName = 'Translators' WHERE userGroupName = 'Translator'"); + Execute.Sql("UPDATE umbracoUserGroup SET userGroupName = 'Writers' WHERE userGroupAlias = 'writer'"); + Execute.Sql("UPDATE umbracoUserGroup SET userGroupName = 'Translators' WHERE userGroupAlias = 'translator'"); + + //Ensure all built in groups have a start node of -1 + Execute.Sql("UPDATE umbracoUserGroup SET startContentId = -1 WHERE userGroupAlias = 'editor'"); + Execute.Sql("UPDATE umbracoUserGroup SET startMediaId = -1 WHERE userGroupAlias = 'editor'"); + Execute.Sql("UPDATE umbracoUserGroup SET startContentId = -1 WHERE userGroupAlias = 'writer'"); + Execute.Sql("UPDATE umbracoUserGroup SET startMediaId = -1 WHERE userGroupAlias = 'writer'"); + Execute.Sql("UPDATE umbracoUserGroup SET startContentId = -1 WHERE userGroupAlias = 'translator'"); + Execute.Sql("UPDATE umbracoUserGroup SET startMediaId = -1 WHERE userGroupAlias = 'translator'"); + Execute.Sql("UPDATE umbracoUserGroup SET startContentId = -1 WHERE userGroupAlias = 'admin'"); + Execute.Sql("UPDATE umbracoUserGroup SET startMediaId = -1 WHERE userGroupAlias = 'admin'"); } private void MigrateUserPermissions() diff --git a/src/Umbraco.Core/PropertyEditors/PropertyEditorAttribute.cs b/src/Umbraco.Core/PropertyEditors/PropertyEditorAttribute.cs index 41e4ccc74e..ec87af29ad 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyEditorAttribute.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyEditorAttribute.cs @@ -26,7 +26,7 @@ namespace Umbraco.Core.PropertyEditors public PropertyEditorAttribute(string alias, string name) { - Mandate.ParameterNotNullOrEmpty(alias, "id"); + Mandate.ParameterNotNullOrEmpty(alias, "alias"); Mandate.ParameterNotNullOrEmpty(name, "name"); Alias = alias; @@ -82,4 +82,4 @@ namespace Umbraco.Core.PropertyEditors /// public string Group { get; set; } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/Services/EntityService.cs b/src/Umbraco.Core/Services/EntityService.cs index a47d552ba5..6e4b5ef17c 100644 --- a/src/Umbraco.Core/Services/EntityService.cs +++ b/src/Umbraco.Core/Services/EntityService.cs @@ -370,13 +370,19 @@ namespace Umbraco.Core.Services public IEnumerable GetPagedDescendants(IEnumerable ids, UmbracoObjectTypes umbracoObjectType, long pageIndex, int pageSize, out long totalRecords, string orderBy = "path", Direction orderDirection = Direction.Ascending, string filter = "") { + totalRecords = 0; + + var idsA = ids.ToArray(); + if (idsA.Length == 0) + return Enumerable.Empty(); + var objectTypeId = umbracoObjectType.GetGuid(); + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { var repository = RepositoryFactory.CreateEntityRepository(uow); var query = Query.Builder; - var idsA = ids.ToArray(); if (idsA.All(x => x != Constants.System.Root)) { var clauses = new List>>(); diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index 572453109d..7da5022d31 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -295,6 +295,18 @@ namespace Umbraco.Core.Services var repository = RepositoryFactory.CreateUserRepository(uow); repository.AddOrUpdate(entity); + + //Now we have to check for backwards compat hacks + var explicitUser = entity as User; + if (explicitUser != null && explicitUser.GroupsToSave.Count > 0) + { + var groupRepository = RepositoryFactory.CreateUserGroupRepository(uow); + foreach (var userGroup in explicitUser.GroupsToSave) + { + groupRepository.AddOrUpdate(userGroup); + } + } + try { // try to flush the unit of work @@ -340,17 +352,28 @@ namespace Umbraco.Core.Services } } var repository = RepositoryFactory.CreateUserRepository(uow); - foreach (var member in asArray) + var groupRepository = RepositoryFactory.CreateUserGroupRepository(uow); + foreach (var user in asArray) { - if (string.IsNullOrWhiteSpace(member.Username)) + if (string.IsNullOrWhiteSpace(user.Username)) { throw new ArgumentException("Cannot save user with empty username."); } - if (string.IsNullOrWhiteSpace(member.Name)) + if (string.IsNullOrWhiteSpace(user.Name)) { throw new ArgumentException("Cannot save user with empty name."); } - repository.AddOrUpdate(member); + repository.AddOrUpdate(user); + + //Now we have to check for backwards compat hacks + var explicitUser = user as User; + if (explicitUser != null && explicitUser.GroupsToSave.Count > 0) + { + foreach (var userGroup in explicitUser.GroupsToSave) + { + groupRepository.AddOrUpdate(userGroup); + } + } } //commit the whole lot in one go uow.Commit(); diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 87206aa1c6..087029786a 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -70,21 +70,17 @@ ..\packages\Microsoft.AspNet.Identity.Owin.2.2.1\lib\net45\Microsoft.AspNet.Identity.Owin.dll True - - ..\packages\Microsoft.Owin.3.0.1\lib\net45\Microsoft.Owin.dll - True + + ..\packages\Microsoft.Owin.3.1.0\lib\net45\Microsoft.Owin.dll - - ..\packages\Microsoft.Owin.Security.3.0.1\lib\net45\Microsoft.Owin.Security.dll - True + + ..\packages\Microsoft.Owin.Security.3.1.0\lib\net45\Microsoft.Owin.Security.dll - - ..\packages\Microsoft.Owin.Security.Cookies.3.0.1\lib\net45\Microsoft.Owin.Security.Cookies.dll - True + + ..\packages\Microsoft.Owin.Security.Cookies.3.1.0\lib\net45\Microsoft.Owin.Security.Cookies.dll - - ..\packages\Microsoft.Owin.Security.OAuth.3.0.1\lib\net45\Microsoft.Owin.Security.OAuth.dll - True + + ..\packages\Microsoft.Owin.Security.OAuth.3.1.0\lib\net45\Microsoft.Owin.Security.OAuth.dll ..\packages\MiniProfiler.2.1.0\lib\net40\MiniProfiler.dll @@ -351,10 +347,12 @@ + + diff --git a/src/Umbraco.Core/app.config b/src/Umbraco.Core/app.config index 9bec1bdd87..5d31bd6363 100644 --- a/src/Umbraco.Core/app.config +++ b/src/Umbraco.Core/app.config @@ -16,19 +16,19 @@ - + - + - + - + diff --git a/src/Umbraco.Core/packages.config b/src/Umbraco.Core/packages.config index dd00d61ea6..c883ef1361 100644 --- a/src/Umbraco.Core/packages.config +++ b/src/Umbraco.Core/packages.config @@ -7,10 +7,10 @@ - - - - + + + + diff --git a/src/Umbraco.Tests.Benchmarks/App.config b/src/Umbraco.Tests.Benchmarks/App.config index b63af79365..e29f2348e4 100644 --- a/src/Umbraco.Tests.Benchmarks/App.config +++ b/src/Umbraco.Tests.Benchmarks/App.config @@ -13,19 +13,19 @@ - + - + - + - + diff --git a/src/Umbraco.Tests/App.config b/src/Umbraco.Tests/App.config index 2eb045e988..50dab9fc4b 100644 --- a/src/Umbraco.Tests/App.config +++ b/src/Umbraco.Tests/App.config @@ -165,7 +165,7 @@ - + @@ -181,7 +181,7 @@ - + diff --git a/src/Umbraco.Tests/Models/UserExtensionsTests.cs b/src/Umbraco.Tests/Models/UserExtensionsTests.cs index 2b1f6c9e37..1e6c7664de 100644 --- a/src/Umbraco.Tests/Models/UserExtensionsTests.cs +++ b/src/Umbraco.Tests/Models/UserExtensionsTests.cs @@ -14,12 +14,15 @@ namespace Umbraco.Tests.Models [TestFixture] public class UserExtensionsTests { - [TestCase(2, "-1,1,2", "-1,1,2,3,4,5", true)] - [TestCase(6, "-1,1,2,3,4,5,6", "-1,1,2,3,4,5", false)] - [TestCase(-1, "-1", "-1,1,2,3,4,5", true)] - [TestCase(5, "-1,1,2,3,4,5", "-1,1,2,3,4,5", true)] - [TestCase(-1, "-1", "-1,-20,1,2,3,4,5", true)] - [TestCase(1, "-1,-20,1", "-1,-20,1,2,3,4,5", false)] + [TestCase(-1, "-1", "-1,1,2,3,4,5", true)] // below root start node + [TestCase(2, "-1,1,2", "-1,1,2,3,4,5", true)] // below start node + [TestCase(5, "-1,1,2,3,4,5", "-1,1,2,3,4,5", true)] // at start node + + [TestCase(6, "-1,1,2,3,4,5,6", "-1,1,2,3,4,5", false)] // above start node + + [TestCase(-1, "-1", "-1,-20,1,2,3,4,5", true)] // below root start node, bin + [TestCase(1, "-1,-20,1", "-1,-20,1,2,3,4,5", false)] // below bin start node + public void Determines_Path_Based_Access_To_Content(int startNodeId, string startNodePath, string contentPath, bool outcome) { var userMock = new Mock(); @@ -27,15 +30,12 @@ namespace Umbraco.Tests.Models var user = userMock.Object; var content = Mock.Of(c => c.Path == contentPath && c.Id == 5); - var entityServiceMock = new Mock(); - entityServiceMock.Setup(x => x.GetAll(It.IsAny(), It.IsAny())) - .Returns(new[] - { - Mock.Of(entity => entity.Id == startNodeId && entity.Path == startNodePath) - }); - var entityService = entityServiceMock.Object; + var esmock = new Mock(); + esmock + .Setup(x => x.GetAllPaths(It.IsAny(), It.IsAny())) + .Returns((type, ids) => new [] { new EntityPath { Id = startNodeId, Path = startNodePath } }); - Assert.AreEqual(outcome, user.HasPathAccess(content, entityService)); + Assert.AreEqual(outcome, user.HasPathAccess(content, esmock.Object)); } [TestCase("", "1", "1")] // single user start, top level @@ -52,6 +52,9 @@ namespace Umbraco.Tests.Models [TestCase("3", "2,5", "2,5")] // user and group start, restrict [TestCase("3", "2,1", "2,1")] // user and group start, expand + [TestCase("3,8", "2,6", "3,2")] // exclude bin + [TestCase("", "6", "")] // exclude bin + public void CombineStartNodes(string groupSn, string userSn, string expected) { // 1 @@ -59,15 +62,23 @@ namespace Umbraco.Tests.Models // 5 // 2 // 4 + // bin + // 6 + // 7 + // 8 var paths = new Dictionary { - { 1, "-1, 1" }, - { 2, "-1, 2" }, - { 3, "-1, 1, 3" }, - { 4, "-1, 2, 4" }, - { 5, "-1, 1, 3, 5" }, + { 1, "-1,1" }, + { 2, "-1,2" }, + { 3, "-1,1,3" }, + { 4, "-1,2,4" }, + { 5, "-1,1,3,5" }, + { 6, "-1,-20,6" }, + { 7, "-1,-20,7" }, + { 8, "-1,-20,7,8" }, }; + var esmock = new Mock(); esmock .Setup(x => x.GetAllPaths(It.IsAny(), It.IsAny())) diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentExtensionTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentExtensionTests.cs index e2e1b91087..8d004e33f0 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentExtensionTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentExtensionTests.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Text; using NUnit.Framework; using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; using Umbraco.Tests.TestHelpers; using Umbraco.Web; @@ -16,8 +17,16 @@ namespace Umbraco.Tests.PublishedContent private UmbracoContext ctx; private string xmlContent = ""; private bool createContentTypes = true; - - protected override string GetXmlContent(int templateId) + + public override void Initialize() + { + base.Initialize(); + + // make sure we get them from the content service + PublishedContentType.GetPublishedContentTypeCallback = null; + } + + protected override string GetXmlContent(int templateId) { return xmlContent; } @@ -57,7 +66,7 @@ namespace Umbraco.Tests.PublishedContent var publishedContent = ctx.ContentCache.GetById(1100); Assert.That(publishedContent.IsDocumentType("base", true)); } - + [Test] public void IsDocumentType_Recursive_InvalidBaseType_ReturnsFalse() { @@ -73,23 +82,23 @@ namespace Umbraco.Tests.PublishedContent if (createContentTypes) { var contentTypeService = ctx.Application.Services.ContentTypeService; - var baseType = new ContentType(-1) {Alias = "base", Name = "Base"}; + var baseType = new ContentType(-1) { Alias = "base", Name = "Base" }; const string contentTypeAlias = "inherited"; - var inheritedType = new ContentType(baseType, contentTypeAlias) {Alias = contentTypeAlias, Name = "Inherited"}; + var inheritedType = new ContentType(baseType, contentTypeAlias) { Alias = contentTypeAlias, Name = "Inherited" }; contentTypeService.Save(baseType); contentTypeService.Save(inheritedType); createContentTypes = false; } - #region setup xml content + xmlContent = @" - ]> "; - #endregion + } } } diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs index 3967afce79..e9af22b968 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs @@ -44,11 +44,11 @@ namespace Umbraco.Tests.PublishedContent var propertyTypes = new[] { // AutoPublishedContentType will auto-generate other properties - new PublishedPropertyType("umbracoNaviHide", 0, Constants.PropertyEditors.TrueFalseAlias), - new PublishedPropertyType("selectedNodes", 0, "?"), - new PublishedPropertyType("umbracoUrlAlias", 0, "?"), - new PublishedPropertyType("content", 0, Constants.PropertyEditors.TinyMCEAlias), - new PublishedPropertyType("testRecursive", 0, "?"), + new PublishedPropertyType("umbracoNaviHide", 0, Constants.PropertyEditors.TrueFalseAlias), + new PublishedPropertyType("selectedNodes", 0, "?"), + new PublishedPropertyType("umbracoUrlAlias", 0, "?"), + new PublishedPropertyType("content", 0, Constants.PropertyEditors.TinyMCEAlias), + new PublishedPropertyType("testRecursive", 0, "?"), }; var compositionAliases = new[] {"MyCompositionAlias"}; var type = new AutoPublishedContentType(0, "anything", compositionAliases, propertyTypes); @@ -73,7 +73,7 @@ namespace Umbraco.Tests.PublishedContent protected override string GetXmlContent(int templateId) { return @" - @@ -87,17 +87,17 @@ namespace Umbraco.Tests.PublishedContent This is some content]]> - + - + - + 1 @@ -211,8 +211,8 @@ namespace Umbraco.Tests.PublishedContent [PublishedContentModel("Home")] internal class Home : PublishedContentModel { - public Home(IPublishedContent content) - : base(content) + public Home(IPublishedContent content) + : base(content) {} } @@ -659,6 +659,28 @@ namespace Umbraco.Tests.PublishedContent Assert.AreEqual((int)1178, (int)result.Id); } + [Test] + public void GetKey() + { + var key = Guid.Parse("CDB83BBC-A83B-4BA6-93B8-AADEF67D3C09"); + + // doc is Home (a model) and GetKey unwraps and works + var doc = GetNode(1176); + Assert.IsInstanceOf(doc); + Assert.AreEqual(key, doc.GetKey()); + + // wrapped is PublishedContentWrapped and WithKey unwraps + var wrapped = new TestWrapped(doc); + Assert.AreEqual(key, wrapped.GetKey()); + } + + class TestWrapped : PublishedContentWrapped + { + public TestWrapped(IPublishedContent content) + : base(content) + { } + } + [Test] public void DetachedProperty1() { diff --git a/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs b/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs index aacb056346..7eb4442541 100644 --- a/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs +++ b/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs @@ -15,7 +15,7 @@ namespace Umbraco.Tests.Scheduling [Timeout(30000)] public class BackgroundTaskRunnerTests { - private ILogger _logger; + private ILogger _logger; [TestFixtureSetUp] public void InitializeFixture() @@ -93,7 +93,7 @@ namespace Umbraco.Tests.Scheduling { runner.Add(new MyTask()); }); - } + } } [Test] @@ -242,7 +242,7 @@ namespace Umbraco.Tests.Scheduling } } - + [Test] public void Create_IsNotRunning() { @@ -252,16 +252,18 @@ namespace Umbraco.Tests.Scheduling } } - + [Test] public async void Create_AutoStart_IsRunning() { using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions { - AutoStart = true + AutoStart = true, + KeepAlive = true // else stops! }, _logger)) { Assert.IsTrue(runner.IsRunning); // because AutoStart is true + runner.Stop(false); // keepalive = must be stopped await runner.StoppedAwaitable; // runner stops, within test's timeout } } @@ -296,7 +298,7 @@ namespace Umbraco.Tests.Scheduling // so that we don't have a runaway task in tests, etc - but it does NOT terminate // the runner - it really is NOT a nice way to end a runner - it's there for tests } - + [Test] public void Startup_KeepAlive_IsRunning() { @@ -359,7 +361,7 @@ namespace Umbraco.Tests.Scheduling } } - + [Test] public async void WaitOnRunner_Tasks() { @@ -504,7 +506,7 @@ namespace Umbraco.Tests.Scheduling //wait till the thread is done await tManager.CurrentThreadingTask; - + foreach (var task in tasks) { Assert.IsTrue(task.Ended != default(DateTime)); @@ -516,7 +518,7 @@ namespace Umbraco.Tests.Scheduling } } - + [Test] public void RecurringTaskTest() { @@ -532,7 +534,7 @@ namespace Umbraco.Tests.Scheduling }; var task = new MyRecurringTask(runner, 200, 500); - + runner.Add(task); Assert.IsTrue(runner.IsRunning); // waiting on delay @@ -577,7 +579,7 @@ namespace Umbraco.Tests.Scheduling runner.Add(task); Assert.IsTrue(runner.IsRunning); Thread.Sleep(5000); - Assert.IsTrue(runner.IsRunning); // still waiting for the task to release + Assert.IsTrue(runner.IsRunning); // still waiting for the task to release Assert.IsFalse(task.HasRun); runner.Shutdown(false, false); await runner.StoppedAwaitable; // wait for the entire runner operation to complete @@ -585,7 +587,7 @@ namespace Umbraco.Tests.Scheduling } } - + [Test] public void LatchedRecurring() { @@ -842,7 +844,7 @@ namespace Umbraco.Tests.Scheduling } public override bool PerformRun() - { + { Thread.Sleep(_runMilliseconds); return true; // repeat } @@ -976,7 +978,7 @@ namespace Umbraco.Tests.Scheduling public virtual Task RunAsync(CancellationToken token) { throw new NotImplementedException(); - //return Task.Delay(500); + //return Task.Delay(500); } public virtual bool IsAsync diff --git a/src/Umbraco.Tests/Web/Controllers/ContentControllerUnitTests.cs b/src/Umbraco.Tests/Web/Controllers/ContentControllerUnitTests.cs index e80e0743d5..22eb3d6398 100644 --- a/src/Umbraco.Tests/Web/Controllers/ContentControllerUnitTests.cs +++ b/src/Umbraco.Tests/Web/Controllers/ContentControllerUnitTests.cs @@ -19,6 +19,7 @@ namespace Umbraco.Tests.Web.Controllers //arrange var userMock = new Mock(); userMock.Setup(u => u.Id).Returns(9); + userMock.Setup(u => u.Groups).Returns(new[] { new ReadOnlyUserGroup(1, "admin", "", -1, -1, "admin", new string[0], new List()) }); var user = userMock.Object; var contentMock = new Mock(); contentMock.Setup(c => c.Path).Returns("-1,1234,5678"); @@ -83,8 +84,8 @@ namespace Umbraco.Tests.Web.Controllers userServiceMock.Setup(x => x.GetPermissionsForPath(user, "-1,1234")).Returns(permissionSet); var userService = userServiceMock.Object; var entityServiceMock = new Mock(); - entityServiceMock.Setup(x => x.GetAll(It.IsAny(), It.IsAny())) - .Returns(new[] { Mock.Of(entity => entity.Id == 9876 && entity.Path == "-1,9876") }); + entityServiceMock.Setup(x => x.GetAllPaths(It.IsAny(), It.IsAny())) + .Returns(new[] { Mock.Of(entity => entity.Id == 9876 && entity.Path == "-1,9876") }); var entityService = entityServiceMock.Object; //act @@ -131,6 +132,7 @@ namespace Umbraco.Tests.Web.Controllers //arrange var userMock = new Mock(); userMock.Setup(u => u.Id).Returns(9); + userMock.Setup(u => u.Groups).Returns(new[] { new ReadOnlyUserGroup(1, "admin", "", -1, -1, "admin", new string[0], new List()) }); var user = userMock.Object; var contentMock = new Mock(); contentMock.Setup(c => c.Path).Returns("-1,1234,5678"); @@ -162,6 +164,7 @@ namespace Umbraco.Tests.Web.Controllers //arrange var userMock = new Mock(); userMock.Setup(u => u.Id).Returns(0); + userMock.Setup(u => u.Groups).Returns(new[] {new ReadOnlyUserGroup(1, "admin", "", -1, -1, "admin", new string[0], new List())}); var user = userMock.Object; var contentServiceMock = new Mock(); var contentService = contentServiceMock.Object; @@ -183,6 +186,7 @@ namespace Umbraco.Tests.Web.Controllers //arrange var userMock = new Mock(); userMock.Setup(u => u.Id).Returns(0); + userMock.Setup(u => u.Groups).Returns(new[] { new ReadOnlyUserGroup(1, "admin", "", -1, -1, "admin", new string[0], new List()) }); var user = userMock.Object; var contentServiceMock = new Mock(); var contentService = contentServiceMock.Object; @@ -211,8 +215,8 @@ namespace Umbraco.Tests.Web.Controllers var userServiceMock = new Mock(); var userService = userServiceMock.Object; var entityServiceMock = new Mock(); - entityServiceMock.Setup(x => x.GetAll(It.IsAny(), It.IsAny())) - .Returns(new[] { Mock.Of(entity => entity.Id == 1234 && entity.Path == "-1,1234") }); + entityServiceMock.Setup(x => x.GetAllPaths(It.IsAny(), It.IsAny())) + .Returns(new[] { Mock.Of(entity => entity.Id == 1234 && entity.Path == "-1,1234") }); var entityService = entityServiceMock.Object; //act @@ -235,8 +239,8 @@ namespace Umbraco.Tests.Web.Controllers var userServiceMock = new Mock(); var userService = userServiceMock.Object; var entityServiceMock = new Mock(); - entityServiceMock.Setup(x => x.GetAll(It.IsAny(), It.IsAny())) - .Returns(new[] { Mock.Of(entity => entity.Id == 1234 && entity.Path == "-1,1234") }); + entityServiceMock.Setup(x => x.GetAllPaths(It.IsAny(), It.IsAny())) + .Returns(new[] { Mock.Of(entity => entity.Id == 1234 && entity.Path == "-1,1234") }); var entityService = entityServiceMock.Object; //act @@ -252,6 +256,7 @@ namespace Umbraco.Tests.Web.Controllers //arrange var userMock = new Mock(); userMock.Setup(u => u.Id).Returns(0); + userMock.Setup(u => u.Groups).Returns(new[] { new ReadOnlyUserGroup(1, "admin", "", -1, -1, "admin", new string[0], new List()) }); var user = userMock.Object; var userServiceMock = new Mock(); @@ -309,6 +314,7 @@ namespace Umbraco.Tests.Web.Controllers //arrange var userMock = new Mock(); userMock.Setup(u => u.Id).Returns(0); + userMock.Setup(u => u.Groups).Returns(new[] { new ReadOnlyUserGroup(1, "admin", "", -1, -1, "admin", new string[0], new List()) }); var user = userMock.Object; var userServiceMock = new Mock(); diff --git a/src/Umbraco.Tests/Web/Controllers/FilterAllowedOutgoingContentAttributeTests.cs b/src/Umbraco.Tests/Web/Controllers/FilterAllowedOutgoingContentAttributeTests.cs index 3bc39ffb29..d9ef2d719e 100644 --- a/src/Umbraco.Tests/Web/Controllers/FilterAllowedOutgoingContentAttributeTests.cs +++ b/src/Umbraco.Tests/Web/Controllers/FilterAllowedOutgoingContentAttributeTests.cs @@ -92,8 +92,8 @@ namespace Umbraco.Tests.Web.Controllers var userServiceMock = new Mock(); var userService = userServiceMock.Object; var entityServiceMock = new Mock(); - entityServiceMock.Setup(x => x.GetAll(It.IsAny(), It.IsAny())) - .Returns(new[] { Mock.Of(entity => entity.Id == 5 && entity.Path == "-1,5") }); + entityServiceMock.Setup(x => x.GetAllPaths(It.IsAny(), It.IsAny())) + .Returns(new[] { Mock.Of(entity => entity.Id == 5 && entity.Path == "-1,5") }); var entityService = entityServiceMock.Object; var att = new FilterAllowedOutgoingContentAttribute(typeof(IEnumerable), userService, entityService); diff --git a/src/Umbraco.Tests/Web/Controllers/MediaControllerUnitTests.cs b/src/Umbraco.Tests/Web/Controllers/MediaControllerUnitTests.cs index 553a37adc9..54692a1196 100644 --- a/src/Umbraco.Tests/Web/Controllers/MediaControllerUnitTests.cs +++ b/src/Umbraco.Tests/Web/Controllers/MediaControllerUnitTests.cs @@ -19,6 +19,7 @@ namespace Umbraco.Tests.Web.Controllers //arrange var userMock = new Mock(); userMock.Setup(u => u.Id).Returns(9); + userMock.Setup(u => u.Groups).Returns(new[] { new ReadOnlyUserGroup(1, "admin", "", -1, -1, "admin", new string[0], new List()) }); var user = userMock.Object; var mediaMock = new Mock(); mediaMock.Setup(m => m.Path).Returns("-1,1234,5678"); @@ -71,8 +72,8 @@ namespace Umbraco.Tests.Web.Controllers mediaServiceMock.Setup(x => x.GetById(1234)).Returns(media); var mediaService = mediaServiceMock.Object; var entityServiceMock = new Mock(); - entityServiceMock.Setup(x => x.GetAll(It.IsAny(), It.IsAny())) - .Returns(new[] {Mock.Of(entity => entity.Id == 9876 && entity.Path == "-1,9876")}); + entityServiceMock.Setup(x => x.GetAllPaths(It.IsAny(), It.IsAny())) + .Returns(new[] {Mock.Of(entity => entity.Id == 9876 && entity.Path == "-1,9876")}); var entityService = entityServiceMock.Object; //act @@ -88,6 +89,7 @@ namespace Umbraco.Tests.Web.Controllers //arrange var userMock = new Mock(); userMock.Setup(u => u.Id).Returns(0); + userMock.Setup(u => u.Groups).Returns(new[] { new ReadOnlyUserGroup(1, "admin", "", -1, -1, "admin", new string[0], new List()) }); var user = userMock.Object; var mediaServiceMock = new Mock(); var mediaService = mediaServiceMock.Object; @@ -112,8 +114,8 @@ namespace Umbraco.Tests.Web.Controllers var mediaServiceMock = new Mock(); var mediaService = mediaServiceMock.Object; var entityServiceMock = new Mock(); - entityServiceMock.Setup(x => x.GetAll(It.IsAny(), It.IsAny())) - .Returns(new[] { Mock.Of(entity => entity.Id == 1234 && entity.Path == "-1,1234") }); + entityServiceMock.Setup(x => x.GetAllPaths(It.IsAny(), It.IsAny())) + .Returns(new[] { Mock.Of(entity => entity.Id == 1234 && entity.Path == "-1,1234") }); var entityService = entityServiceMock.Object; //act @@ -129,6 +131,7 @@ namespace Umbraco.Tests.Web.Controllers //arrange var userMock = new Mock(); userMock.Setup(u => u.Id).Returns(0); + userMock.Setup(u => u.Groups).Returns(new[] { new ReadOnlyUserGroup(1, "admin", "", -1, -1, "admin", new string[0], new List()) }); var user = userMock.Object; var mediaServiceMock = new Mock(); var mediaService = mediaServiceMock.Object; @@ -153,8 +156,8 @@ namespace Umbraco.Tests.Web.Controllers var mediaServiceMock = new Mock(); var mediaService = mediaServiceMock.Object; var entityServiceMock = new Mock(); - entityServiceMock.Setup(x => x.GetAll(It.IsAny(), It.IsAny())) - .Returns(new[] { Mock.Of(entity => entity.Id == 1234 && entity.Path == "-1,1234") }); + entityServiceMock.Setup(x => x.GetAllPaths(It.IsAny(), It.IsAny())) + .Returns(new[] { Mock.Of(entity => entity.Id == 1234 && entity.Path == "-1,1234") }); var entityService = entityServiceMock.Object; //act diff --git a/src/Umbraco.Web.UI.Client/bower.json b/src/Umbraco.Web.UI.Client/bower.json index 70accbdcca..0ae1e2f214 100644 --- a/src/Umbraco.Web.UI.Client/bower.json +++ b/src/Umbraco.Web.UI.Client/bower.json @@ -1,5 +1,5 @@ { - "name": "Umbraco", + "name": "umbraco", "version": "7", "homepage": "https://github.com/umbraco/Umbraco-CMS", "authors": [ 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 9966e94b70..e935557ce9 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 @@ -82,8 +82,10 @@ @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. @param {boolean} allowOpen (binding): Show/Hide the open button. +@param {boolean} allowEdit (binding): Show/Hide the edit button (Added in version 7.7.0). @param {function} onRemove (expression): Callback function when the remove button is clicked. @param {function} onOpen (expression): Callback function when the open button is clicked. +@param {function} onEdit (expression): Callback function when the edit button is clicked (Added in version 7.7.0). **/ (function () { @@ -108,8 +110,10 @@ sortable: "=?", allowOpen: "=?", allowRemove: "=?", + allowEdit: "=?", onOpen: "&?", - onRemove: "&?" + onRemove: "&?", + onEdit: "&?" }, link: link }; 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 667754faaa..ed3bd04f8d 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 @@ -1,8 +1,6 @@ .umb-node-preview { padding: 7px 0; - border-radius: 3px; display: flex; - align-items: center; max-width: 66.6%; box-sizing: border-box; border-bottom: 1px solid @gray-9; @@ -37,10 +35,12 @@ .umb-node-preview__content { flex: 1 1 auto; + margin-right: 25px; } .umb-node-preview__name { color: @black; + margin-top: 2px; } .umb-node-preview__description { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js index 9c5a2b888c..d58234b493 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js @@ -91,7 +91,7 @@ angular.module("umbraco").controller("Umbraco.Overlays.LinkPickerController", userService.getCurrentUser().then(function (userData) { $scope.mediaPickerOverlay = { view: "mediapicker", - startNodeId: userData.startMediaIds.length == 0 ? -1 : userData.startMediaIds[0], + startNodeId: userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0], show: true, submit: function(model) { var media = model.selectedImages[0]; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js index fb38581eac..a6a2ddcbab 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js @@ -16,6 +16,7 @@ angular.module("umbraco") $scope.startNodeId = dialogOptions.startNodeId ? dialogOptions.startNodeId : -1; $scope.cropSize = dialogOptions.cropSize; $scope.lastOpenedNode = localStorageService.get("umbLastOpenedMediaNodeId"); + $scope.lockedFolder = true; var umbracoSettings = Umbraco.Sys.ServerVariables.umbracoSettings; var allowedUploadFiles = mediaHelper.formatFileTypes(umbracoSettings.allowedUploadFiles); @@ -121,6 +122,8 @@ angular.module("umbraco") $scope.path = []; } + $scope.lockedFolder = folder.id === -1 && $scope.model.startNodeIsVirtual; + $scope.currentFolder = folder; localStorageService.set("umbLastOpenedMediaNodeId", folder.id); return getChildren(folder.id); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.html index ab745f0f75..8c726f4a17 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.html @@ -30,7 +30,7 @@ type="button" label-key="general_upload" action="upload()" - disabled="disabled"> + disabled="lockedFolder"> @@ -48,7 +48,7 @@ / -
  • +
  • @@ -67,7 +67,7 @@
    -
    -
    {{ name }}
    -
    {{ description }}
    -
    + +
    {{ name }}
    +
    {{ description }}
    +
    Permissions: @@ -13,6 +13,7 @@
    + Edit Open Remove
    diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/developerdashboardintro.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/developerdashboardintro.html index 6ced082c06..9b7f1433e3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/developerdashboardintro.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/developerdashboardintro.html @@ -10,5 +10,5 @@
  • Find an add-on package to help you get going quickly
  • Watch our tutorial videos (some are free, some require a subscription)
  • Find out about our productivity boosting tools and commercial support
  • -
  • Find out about real-life training and certification opportunities
  • +
  • Find out about real-life training and certification opportunities
  • diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/settingsdashboardintro.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/settingsdashboardintro.html index 3a45776872..d59d10d393 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/settingsdashboardintro.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/settingsdashboardintro.html @@ -10,5 +10,5 @@
  • Ask a question in the Community Forum
  • Watch our tutorial videos (some are free, some require a subscription)
  • Find out about our productivity boosting tools and commercial support
  • -
  • Find out about real-life training and certification opportunities
  • +
  • Find out about real-life training and certification opportunities
  • diff --git a/src/Umbraco.Web.UI.Client/src/views/packager/views/install-local.html b/src/Umbraco.Web.UI.Client/src/views/packager/views/install-local.html index b265a346b3..f548a29308 100644 --- a/src/Umbraco.Web.UI.Client/src/views/packager/views/install-local.html +++ b/src/Umbraco.Web.UI.Client/src/views/packager/views/install-local.html @@ -90,7 +90,7 @@ - Cancel and upload another package + Cancel and upload another package diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js index 57f7d06f80..7198166c25 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js @@ -4,7 +4,8 @@ angular.module("umbraco") if (!$scope.model.config.startNodeId) { userService.getCurrentUser().then(function (userData) { - $scope.model.config.startNodeId = userData.startMediaIds.length === 0 ? -1 : userData.startMediaIds[0]; + $scope.model.config.startNodeId = userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0]; + $scope.model.config.startNodeIsVirtual = userData.startMediaIds.length !== 1; }); } @@ -12,6 +13,7 @@ angular.module("umbraco") $scope.mediaPickerOverlay = {}; $scope.mediaPickerOverlay.view = "mediapicker"; $scope.mediaPickerOverlay.startNodeId = $scope.model.config && $scope.model.config.startNodeId ? $scope.model.config.startNodeId : undefined; + $scope.mediaPickerOverlay.startNodeIsVirtual = $scope.mediaPickerOverlay.startNodeId ? $scope.model.config.startNodeIsVirtual : undefined; $scope.mediaPickerOverlay.cropSize = $scope.control.editor.config && $scope.control.editor.config.size ? $scope.control.editor.config.size : undefined; $scope.mediaPickerOverlay.showDetails = true; $scope.mediaPickerOverlay.disableFolderSelect = true; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js index 6ec9e74fa4..99c3fd80ff 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js @@ -28,7 +28,7 @@ currentTarget: currentTarget, onlyImages: true, showDetails: true, - startNodeId: userData.startMediaIds.length === 0 ? -1 : userData.startMediaIds[0], + startNodeId: userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0], view: "mediapicker", show: true, submit: function(model) { 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 d09c89c445..8a7b20498d 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 @@ -9,8 +9,9 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl var disableFolderSelect = $scope.model.config.disableFolderSelect && $scope.model.config.disableFolderSelect !== '0' ? true : false; if (!$scope.model.config.startNodeId) { - userService.getCurrentUser().then(function (userData) { - $scope.model.config.startNodeId = userData.startMediaIds.length === 0 ? -1 : userData.startMediaIds[0]; + userService.getCurrentUser().then(function(userData) { + $scope.model.config.startNodeId = userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0]; + $scope.model.config.startNodeIsVirtual = userData.startMediaIds.length !== 1; }); } @@ -45,7 +46,7 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl $scope.ids.push(media.udi); } else { - $scope.ids.push(media.id); + $scope.ids.push(media.id); } } }); @@ -73,6 +74,7 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl view: "mediapicker", title: "Select media", startNodeId: $scope.model.config.startNodeId, + startNodeIsVirtual: $scope.model.config.startNodeIsVirtual, multiPicker: multiPicker, onlyImages: onlyImages, disableFolderSelect: disableFolderSelect, diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js index b5beef2ff7..07b9bedbc9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js @@ -286,7 +286,7 @@ angular.module("umbraco") onlyImages: true, showDetails: true, disableFolderSelect: true, - startNodeId: userData.startMediaIds.length === 0 ? -1 : userData.startMediaIds[0], + startNodeId: userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0], view: "mediapicker", show: true, submit: function(model) { diff --git a/src/Umbraco.Web.UI.Client/src/views/users/group.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/group.controller.js index 659a1f0c47..9d58e483e4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/group.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/group.controller.js @@ -19,6 +19,7 @@ vm.clearStartNode = clearStartNode; vm.save = save; vm.openGranularPermissionsPicker = openGranularPermissionsPicker; + vm.setPermissionsForNode = setPermissionsForNode; function init() { @@ -251,8 +252,10 @@ vm.nodePermissions.show = false; vm.nodePermissions = null; // close content picker overlay - vm.contentPicker.show = false; - vm.contentPicker = null; + if(vm.contentPicker) { + vm.contentPicker.show = false; + vm.contentPicker = null; + } }, close: function (oldModel) { vm.nodePermissions.show = false; diff --git a/src/Umbraco.Web.UI.Client/src/views/users/group.html b/src/Umbraco.Web.UI.Client/src/views/users/group.html index 62ac90d56f..2110d8f1f0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/group.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/group.html @@ -115,7 +115,9 @@ name="node.name" permissions="node.allowedPermissions" allow-remove="true" - on-remove="vm.removeSelectedItem($index, vm.userGroup.assignedPermissions)"> + on-remove="vm.removeSelectedItem($index, vm.userGroup.assignedPermissions)" + allow-edit="true" + on-edit="vm.setPermissionsForNode(node)"> ..\packages\Microsoft.IO.RecyclableMemoryStream.1.2.1\lib\net45\Microsoft.IO.RecyclableMemoryStream.dll True - - ..\packages\Microsoft.Owin.3.0.1\lib\net45\Microsoft.Owin.dll - True + + ..\packages\Microsoft.Owin.3.1.0\lib\net45\Microsoft.Owin.dll - - False - ..\packages\Microsoft.Owin.Host.SystemWeb.3.0.1\lib\net45\Microsoft.Owin.Host.SystemWeb.dll + + ..\packages\Microsoft.Owin.Host.SystemWeb.3.1.0\lib\net45\Microsoft.Owin.Host.SystemWeb.dll - - False - ..\packages\Microsoft.Owin.Security.3.0.1\lib\net45\Microsoft.Owin.Security.dll + + ..\packages\Microsoft.Owin.Security.3.1.0\lib\net45\Microsoft.Owin.Security.dll - - False - ..\packages\Microsoft.Owin.Security.Cookies.3.0.1\lib\net45\Microsoft.Owin.Security.Cookies.dll + + ..\packages\Microsoft.Owin.Security.Cookies.3.1.0\lib\net45\Microsoft.Owin.Security.Cookies.dll - - False - ..\packages\Microsoft.Owin.Security.OAuth.3.0.1\lib\net45\Microsoft.Owin.Security.OAuth.dll + + ..\packages\Microsoft.Owin.Security.OAuth.3.1.0\lib\net45\Microsoft.Owin.Security.OAuth.dll True diff --git a/src/Umbraco.Web.UI/packages.config b/src/Umbraco.Web.UI/packages.config index 8b22708406..17c4916133 100644 --- a/src/Umbraco.Web.UI/packages.config +++ b/src/Umbraco.Web.UI/packages.config @@ -23,11 +23,11 @@ - - - - - + + + + + diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/pl.xml b/src/Umbraco.Web.UI/umbraco/config/lang/pl.xml index 5c6088bac0..e387ab37ee 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/pl.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/pl.xml @@ -173,6 +173,8 @@ także do obecnego węzła, o ile poniższa domena również do niego należy.]] Cel Oznacza to następującą godzinę na serwerze: Co to oznacza?]]> + Dodaj kolejne pole tekstowe + Usuń te pole tekstowe Kliknij, aby załadować plik @@ -300,6 +302,7 @@ także do obecnego węzła, o ile poniższa domena również do niego należy.]] Link do strony Otwórz zlinkowany dokument w nowym oknie lub zakładce Link do mediów + Link do plików Wybierz media Wybierz ikonę Wybierz element @@ -319,6 +322,7 @@ także do obecnego węzła, o ile poniższa domena również do niego należy.]] Odlinkuj swój konto Wybierz edytora + Wybierz snippet Powiązane arkusze stylów Pokaż etykietę Szerokość i wysokość + Wszystkie typy właściwości & dane właściwości + używające tego typu danych zostaną usunięte na zawsze, potwierdź, że chcesz je także usunąć + Tak, usuń + i wszystkie typy właściwości & dane właściwości używające tego typu danych/key> + Wybierz folder do przeniesienia + do w strukturze drzewa poniżej + został przeniesiony poniżej Dane zostały zapisane, lecz wystąpiły błędy, które musisz poprawić przed publikacją strony: @@ -434,6 +445,7 @@ Możesz dodać dodatkowe języki w menu "Języki" po lewej stronie.]]> Zamknij okno Komentarz Potwierdzenie + Zachowaj Zachowaj proporcje Kontynuuj Kopiuj @@ -445,6 +457,7 @@ Możesz dodać dodatkowe języki w menu "Języki" po lewej stronie.]]> Usunięto Usuwanie... Wygląd + Słownik Rozmiary Dół Pobierz @@ -454,6 +467,7 @@ Możesz dodać dodatkowe języki w menu "Języki" po lewej stronie.]]> Email Błąd Znajdź + Pierwszy Wysokość Pomoc Ikona @@ -464,7 +478,8 @@ Możesz dodać dodatkowe języki w menu "Języki" po lewej stronie.]]> Nieprawidłowe Wyrównaj Język - układ + Ostatni + Układ Ładowanie Zablokowany Zaloguj @@ -491,9 +506,11 @@ Możesz dodać dodatkowe języki w menu "Języki" po lewej stronie.]]> E-mail, aby otrzymywać dane z formularzy Kosz Pozostało + Usuń Zmień nazwę Odnów Wymagany + Odzyskaj Ponów próbę Uprawnienia Szukaj @@ -802,6 +819,39 @@ Naciśnij przycisk instaluj, aby zainstalować bazę danych Umb Wskaż pakiet z Twojego komputera, poprzez kliknięcie na przycisk "Przeglądaj"
    i wskaż gdzie jest zapisany. Pakiety Umbraco przeważnie posiadają rozszerzenie ".umb" lub ".zip". ]]> + Upuść, aby załadować + lub kliknij tutaj, aby wybrać pliki + Załaduj pakiet + Zainstaluj lokalny pakiet poprzez wybranie go ze swojego komputera. Instaluj jedynie te pakiety, z zaufanych i znanych Tobie źródeł + Załaduj kolejny pakiet + Anuluj i załaduj kolejny pakiet + Licencja + Zgadzam się + zasady użytkowania + Zainstaluj pakiet + Zakończ + Zainstalowane pakiety + Nie masz żadnych zainstalowanych pakietów + 'Pakiety' w prawym górnym rogu ekranu]]> + Szukaj pakietów + Wyniki dla + Nie mogliśmy znaleźć niczego dla + Spróbuj wyszukać kolejny pakiet lub przeszukaj kategorie pakietów + Popularne + Nowe wydania + ma + punktów karmy + Informacja + Właściciel + Kontrybutor + Utworzone + Obecna wersja + wersja .NET + Pobrania + Polubienia + Zgodność + Według raportów członków społeczności, ten pakiet jest zgodny z następującymi wersjami Umbraco. Pełna zgodność nie może być zagwarantowana dla wersji zaraportowanych poniżej 100% + Zewnętrzne źródła Autor Demonstracja Dokumentacja @@ -837,6 +887,7 @@ Naciśnij przycisk instaluj, aby zainstalować bazę danych Umb Restartowanie, proszę czekać... Wszystko skończone, Twoja przeglądarka się teraz odświeży, proszę czekać... Proszę kliknąć Zakończ, aby zakończyć instalację i przeładować stronę. + Wgrywanie pakietu... Wklej z zachowaniem formatowania (Nie zalecane) @@ -904,6 +955,10 @@ Naciśnij przycisk instaluj, aby zainstalować bazę danych Umb Resetuj + Zdefiniuj przycięcie + Ustaw alias dla przycięcia, a także jego domyślną szerokość i długość + Zapisz przycięcie + Dodaj nowe przycięcie Aktualna wersja @@ -1112,6 +1167,7 @@ Naciśnij przycisk instaluj, aby zainstalować bazę danych Umb Konstruktor zapytań + Zbuduj zapytanie Element zwrócony, w Chcę @@ -1257,25 +1313,36 @@ Naciśnij przycisk instaluj, aby zainstalować bazę danych Umb + Dodaj pole zastępcze + Pole zastępcze + Dodaj domyślną wartość + Domyślna wartość Pole alternatywne Tekst alternatywny Wielkość liter Kodowanie Wybierz pole Konwertuj złamania wiersza + Tak, konwertuj złamania wiersza Zamienia złamania wiersza na html-tag &lt;br&gt; Niestandardowe Pola Tak, tylko data + Format i kodowanie Formatuj jako datę + Formatuj wartość jako datę lub jako datę i czas, zgodnie z aktywną kulturą Kodowanie HTML Zamienia znaki specjalne na ich odpowiedniki HTML Zostanie wstawione za wartością pola Zostanie wstawione przed wartością pola małe znaki + Modyfikuj dane wyjściowe Nic + Próbka danych wyjściowych Wstaw za polem Wstaw przed polem Rekurencyjne + Tak, spraw, aby było to rekurencyjne + Separator Standardowe Pola Wielkie litery Kodowanie URL @@ -1423,6 +1490,14 @@ Naciśnij przycisk instaluj, aby zainstalować bazę danych Umb Waliduj jako URL ...lub wpisz niestandardową walidację Pole jest wymagane + Wprowadź wyrażenie regularne + Musisz dodać przynajmniej + Możesz mieć jedynie + elementy + wybrane elementy + Niepoprawna data + To nie jest numer + Niepoprawny e-mail