Port 7.7 - WIP

This commit is contained in:
Stephan
2017-09-19 15:51:47 +02:00
parent d54658009c
commit 9ed6576908
126 changed files with 3447 additions and 596 deletions
@@ -82,7 +82,7 @@ namespace Umbraco.Core.Configuration
catch (Exception ex)
{
//invalid path format or something... try/catch to be safe
LogHelper.Error<ClientDependencyConfiguration>("Could not get path from ClientDependency.config", ex);
_logger.Error<ClientDependencyConfiguration>("Could not get path from ClientDependency.config", ex);
}
var success = true;
@@ -99,7 +99,7 @@ namespace Umbraco.Core.Configuration
catch (Exception ex)
{
// Something could be locking the directory or the was another error, making sure we don't break the upgrade installer
LogHelper.Error<ClientDependencyConfiguration>("Could not clear temp files", ex);
_logger.Error<ClientDependencyConfiguration>("Could not clear temp files", ex);
success = false;
}
}
+1 -1
View File
@@ -419,7 +419,7 @@ namespace Umbraco.Core.Models.Membership
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);
var realGroup = customUserGroup as IUserGroup ?? Current.Services.UserService.GetUserGroupById(customUserGroup.Id);
realGroup.AddAllowedSection(sectionAlias);
//now we need to flag this for saving (hack!)
GroupsToSave.Add(realGroup);
@@ -252,7 +252,7 @@ namespace Umbraco.Core.Persistence.Repositories
public Dictionary<string, Guid> GetDictionaryItemKeyMap()
{
var columns = new[] { "key", "id" }.Select(x => (object) SqlSyntax.GetQuotedColumnName(x)).ToArray();
var sql = new Sql().Select(columns).From<DictionaryDto>(SqlSyntax);
var sql = UnitOfWork.Sql().Select(columns).From<DictionaryDto>();
return Database.Fetch<DictionaryItemKeyIdDto>(sql).ToDictionary(x => x.Key, x => x.Id);
}
@@ -1,29 +1,10 @@
using System;
using System.Xml.Linq;
using Umbraco.Core.Models;
namespace Umbraco.Core.Persistence.Repositories
{
public interface IMediaRepository : IRepositoryVersionable<int, IMedia>, IRecycleBinRepository<IMedia>, IReadRepository<Guid, IMedia>
{
/// <summary>
/// Used to add/update published xml for the media item
/// </summary>
/// <param name="content"></param>
/// <param name="xml"></param>
void AddOrUpdateContentXml(IMedia content, Func<IMedia, XElement> xml);
/// <summary>
/// Used to remove the content xml for a content item
/// </summary>
/// <param name="content"></param>
void DeleteContentXml(IMedia content);
/// <summary>
/// Used to add/update preview xml for the content item
/// </summary>
/// <param name="content"></param>
/// <param name="xml"></param>
void AddOrUpdatePreviewXml(IMedia content, Func<IMedia, XElement> xml);
IMedia GetMediaByPath(string mediaPath);
}
}
@@ -600,7 +600,7 @@ ORDER BY colName";
/// </remarks>
public IEnumerable<IUser> GetPagedResultsByQuery(IQuery<IUser> query, long pageIndex, int pageSize, out long totalRecords,
Expression<Func<IUser, object>> orderBy, Direction orderDirection = Direction.Ascending,
string[] userGroups = null, UserState[] userState = null, IQuery<IUser> filter = null)
string[] includeUserGroups = null, string[] excludeUserGroups = null, UserState[] userState = null, IQuery<IUser> filter = null)
{
if (orderBy == null) throw new ArgumentNullException(nameof(orderBy));
@@ -129,7 +129,7 @@ namespace Umbraco.Core.Persistence
/// </summary>
internal bool EnableSqlCount
{
get { return _enableCount; }
get => _enableCount;
set
{
_enableCount = value;
@@ -7,6 +7,7 @@ using System.Web.Security;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin.Security.DataProtection;
using Umbraco.Core.Composing;
using Umbraco.Core.Configuration;
using Umbraco.Core.Configuration.UmbracoSettings;
using Umbraco.Core.Models.Identity;
@@ -49,20 +50,6 @@ namespace Umbraco.Core.Security
#region Static Create methods
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("Use the overload specifying all dependencies instead")]
public static BackOfficeUserManager Create(
IdentityFactoryOptions<BackOfficeUserManager> options,
IUserService userService,
IExternalLoginService externalLoginService,
MembershipProviderBase membershipProvider)
{
return Create(options, userService,
ApplicationContext.Current.Services.EntityService,
externalLoginService, membershipProvider,
UmbracoConfig.For.UmbracoSettings().Content);
}
/// <summary>
/// Creates a BackOfficeUserManager instance with all default options and the default BackOfficeUserManager
/// </summary>
@@ -350,6 +350,7 @@ namespace Umbraco.Core.Security
//Special cases to allow changing password without validating existing credentials
// * the member is new and doesn't have a password set
// * during installation to set the admin password
var installing = Current.RuntimeState.Level == RuntimeLevel.Install;
if (AllowManuallyChangingPassword == false
&& (rawPasswordValue.StartsWith(Constants.Security.EmptyPasswordPrefix)
|| (installing && oldPassword == "default")))
@@ -411,9 +411,9 @@ namespace Umbraco.Core.Services
public Dictionary<string, Guid> GetDictionaryItemKeyMap()
{
using (var uow = UowProvider.GetUnitOfWork(readOnly: true))
using (var uow = UowProvider.CreateUnitOfWork(readOnly: true))
{
var repository = RepositoryFactory.CreateDictionaryRepository(uow);
var repository = uow.CreateRepository<IDictionaryRepository>();
return repository.GetDictionaryItemKeyMap();
}
}
+5 -5
View File
@@ -111,7 +111,7 @@ namespace Umbraco.Core.Services
var parent = GetById(parentId);
return CreateMedia(name, parent, mediaTypeAlias, userId);
}
/// <summary>
/// Creates an <see cref="IMedia"/> object of a specified media type.
/// </summary>
@@ -378,7 +378,7 @@ namespace Umbraco.Core.Services
return repository.GetAll(idsA);
}
}
/// <summary>
/// Gets a collection of <see cref="IMedia"/> objects by the Id of the <see cref="IMediaType"/>
/// </summary>
@@ -632,7 +632,7 @@ namespace Umbraco.Core.Services
var query = uow.Query<IMedia>();
//if the id is System Root, then just get all
if (id != Constants.System.Root)
{
{
var entityRepository = uow.CreateRepository<IEntityRepository>();
var mediaPath = entityRepository.GetAllPaths(Constants.ObjectTypes.MediaGuid, id).ToArray();
if (mediaPath.Length == 0)
@@ -797,7 +797,7 @@ namespace Umbraco.Core.Services
using (var uow = UowProvider.CreateUnitOfWork())
{
var saveEventArgs = new SaveEventArgs<IMedia>(media, evtMsgs);
var saveEventArgs = new SaveEventArgs<IMedia>(media, evtMsgs);
if (raiseEvents && uow.Events.DispatchCancelable(Saving, this, saveEventArgs))
{
uow.Complete();
@@ -1170,7 +1170,7 @@ namespace Umbraco.Core.Services
.ToArray();
moveEventArgs.MoveInfoCollection = moveInfo;
moveEventArgs.CanCancel = false;
moveEventArgs.CanCancel = false;
uow.Events.Dispatch(Moved, this, moveEventArgs);
Audit(uow, AuditType.Move, "Move Media performed by user", userId, media.Id);
uow.Complete();
@@ -132,17 +132,18 @@ namespace Umbraco.Core.Services
var saveEventArgs = new SaveEventArgs<PublicAccessEntry>(entry, evtMsgs);
if (uow.Events.DispatchCancelable(Saving, this, saveEventArgs))
{
uow.Commit();
uow.Complete();
return OperationStatus.Attempt.Cancel(evtMsgs, entry);
}
repo.AddOrUpdate(entry);
uow.Complete();
saveEventArgs.CanCancel = false;
uow.Events.Dispatch(Saved, this, saveEventArgs);
}
saveEventArgs.CanCancel = false;
uow.Events.Dispatch(Saved, this, saveEventArgs);
return OperationStatus.Attempt.Succeed(evtMsgs, entry);
}
@@ -171,16 +172,17 @@ namespace Umbraco.Core.Services
var saveEventArgs = new SaveEventArgs<PublicAccessEntry>(entry, evtMsgs);
if (uow.Events.DispatchCancelable(Saving, this, saveEventArgs))
{
uow.Commit();
uow.Complete();
return OperationStatus.Attempt.Cancel(evtMsgs);
}
repo.AddOrUpdate(entry);
uow.Complete();
saveEventArgs.CanCancel = false;
uow.Events.Dispatch(Saved, this, saveEventArgs);
}
saveEventArgs.CanCancel = false;
uow.Events.Dispatch(Saved, this, saveEventArgs);
return OperationStatus.Attempt.Succeed(evtMsgs);
}
@@ -197,17 +199,18 @@ namespace Umbraco.Core.Services
var saveEventArgs = new SaveEventArgs<PublicAccessEntry>(entry, evtMsgs);
if (uow.Events.DispatchCancelable(Saving, this, saveEventArgs))
{
uow.Commit();
uow.Complete();
return OperationStatus.Attempt.Cancel(evtMsgs);
}
var repo = uow.CreateRepository<IPublicAccessRepository>();
repo.AddOrUpdate(entry);
uow.Complete();
saveEventArgs.CanCancel = false;
uow.Events.Dispatch(Saved, this, saveEventArgs);
}
saveEventArgs.CanCancel = false;
uow.Events.Dispatch(Saved, this, saveEventArgs);
return OperationStatus.Attempt.Succeed(evtMsgs);
}
@@ -224,17 +227,18 @@ namespace Umbraco.Core.Services
var deleteEventArgs = new DeleteEventArgs<PublicAccessEntry>(entry, evtMsgs);
if (uow.Events.DispatchCancelable(Deleting, this, deleteEventArgs))
{
uow.Commit();
return OperationStatus.Cancelled(evtMsgs);
uow.Complete();
return OperationStatus.Attempt.Cancel(evtMsgs);
}
var repo = uow.CreateRepository<IPublicAccessRepository>();
repo.Delete(entry);
uow.Complete();
deleteEventArgs.CanCancel = false;
uow.Events.Dispatch(Deleted, this, deleteEventArgs);
}
deleteEventArgs.CanCancel = false;
uow.Events.Dispatch(Deleted, this, deleteEventArgs);
return OperationStatus.Attempt.Succeed(evtMsgs);
}
-1
View File
@@ -55,7 +55,6 @@ namespace Umbraco.Core
{ PartialView, UdiType.StringUdi},
{ PartialViewMacro, UdiType.StringUdi},
{ Stylesheet, UdiType.StringUdi},
{ UserControl, UdiType.StringUdi},
{ Xslt, UdiType.StringUdi},
};
}
+4
View File
@@ -54,6 +54,9 @@
<ItemGroup>
<!-- note: NuGet deals with transitive references now -->
<PackageReference Include="AutoMapper" Version="6.1.1" />
<PackageReference Include="ClientDependency">
<Version>1.9.2</Version>
</PackageReference>
<!-- do not update -->
<PackageReference Include="HtmlAgilityPack" Version="1.5.1" />
<PackageReference Include="ImageProcessor" Version="2.5.4" />
@@ -1311,6 +1314,7 @@
<Compile Include="Security\MembershipProviderPasswordValidator.cs" />
<Compile Include="Security\OwinExtensions.cs" />
<Compile Include="Security\UmbracoBackOfficeIdentity.cs" />
<Compile Include="Security\UmbracoEmailMessage.cs" />
<Compile Include="Security\UmbracoMembershipProviderBase.cs" />
<Compile Include="Security\UserAwareMembershipProviderPasswordHasher.cs" />
<Compile Include="Security\UserData.cs" />
@@ -7,10 +7,10 @@ namespace Umbraco.Tests.Cache
public class CacheRefresherTests
{
[TestCase("", "123456", "testmachine", true)] //empty hash will continue
[TestCase("fffffff28449cf33", "123456", "testmachine", false)] //match, don't continue
[TestCase("fffffff28449cf33", "12345", "testmachine", true)] // no match, continue
[TestCase("fffffff28449cf33", "123456", "testmachin", true)] // same
[TestCase("fffffff28449cf3", "123456", "testmachine", true)] // same
[TestCase("2e6deefea4444a69dbd15a01b4c2749d", "123456", "testmachine", false)] //match, don't continue
[TestCase("2e6deefea4444a69dbd15a01b4c2749d", "12345", "testmachine", true)] // no match, continue
[TestCase("2e6deefea4444a69dbd15a01b4c2749d", "123456", "testmachin", true)] // same
[TestCase("2e6deefea4444a69dbd15a01b4c2749", "123456", "testmachine", true)] // same
public void Continue_Refreshing_For_Request(string hash, string appDomainAppId, string machineName, bool expected)
{
if (expected)
+144 -7
View File
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using LightInject;
using Moq;
using Newtonsoft.Json;
@@ -7,6 +9,7 @@ using NUnit.Framework;
using Umbraco.Core;
using Umbraco.Core.Cache;
using Umbraco.Core.Composing;
using Umbraco.Core.Deploy;
using Umbraco.Core.Logging;
using Umbraco.Core.Serialization;
@@ -31,7 +34,7 @@ namespace Umbraco.Tests.CoreThings
}
[Test]
public void StringEntityCtorTest()
public void StringUdiCtorTest()
{
var udi = new StringUdi(Constants.UdiEntityType.AnyString, "test-id");
Assert.AreEqual(Constants.UdiEntityType.AnyString, udi.EntityType);
@@ -40,7 +43,7 @@ namespace Umbraco.Tests.CoreThings
}
[Test]
public void StringEntityParseTest()
public void StringUdiParseTest()
{
var udi = Udi.Parse("umb://" + Constants.UdiEntityType.AnyString + "/test-id");
Assert.AreEqual(Constants.UdiEntityType.AnyString, udi.EntityType);
@@ -49,6 +52,9 @@ namespace Umbraco.Tests.CoreThings
Assert.IsNotNull(stringEntityId);
Assert.AreEqual("test-id", stringEntityId.Id);
Assert.AreEqual("umb://" + Constants.UdiEntityType.AnyString + "/test-id", udi.ToString());
udi = Udi.Parse("umb://" + Constants.UdiEntityType.AnyString + "/DA845952BE474EE9BD6F6194272AC750");
Assert.IsInstanceOf<StringUdi>(udi);
}
[Test]
@@ -103,7 +109,7 @@ namespace Umbraco.Tests.CoreThings
}
[Test]
public void GuidEntityCtorTest()
public void GuidUdiCtorTest()
{
var guid = Guid.NewGuid();
var udi = new GuidUdi(Constants.UdiEntityType.AnyGuid, guid);
@@ -113,7 +119,7 @@ namespace Umbraco.Tests.CoreThings
}
[Test]
public void GuidEntityParseTest()
public void GuidUdiParseTest()
{
var guid = Guid.NewGuid();
var s = "umb://" + Constants.UdiEntityType.AnyGuid + "/" + guid.ToString("N");
@@ -168,9 +174,33 @@ namespace Umbraco.Tests.CoreThings
Assert.AreEqual(Constants.UdiEntityType.AnyGuid, udi.EntityType);
Assert.AreEqual(guid, ((GuidUdi)udi).Guid);
Assert.Throws<InvalidOperationException>(() => Udi.Create(Constants.UdiEntityType.AnyString, guid));
Assert.Throws<InvalidOperationException>(() => Udi.Create(Constants.UdiEntityType.AnyGuid, "foo"));
Assert.Throws<ArgumentException>(() => Udi.Create("barf", "foo"));
// *not* testing whether Udi.Create(type, invalidValue) throws
// because we don't throw anymore - see U4-10409
}
[Test]
public void RootUdiTest()
{
var stringUdi = new StringUdi(Constants.UdiEntityType.AnyString, string.Empty);
Assert.IsTrue(stringUdi.IsRoot);
Assert.AreEqual("umb://any-string/", stringUdi.ToString());
var guidUdi = new GuidUdi(Constants.UdiEntityType.AnyGuid, Guid.Empty);
Assert.IsTrue(guidUdi.IsRoot);
Assert.AreEqual("umb://any-guid/00000000000000000000000000000000", guidUdi.ToString());
var udi = Udi.Parse("umb://any-string/");
Assert.IsTrue(udi.IsRoot);
Assert.IsInstanceOf<StringUdi>(udi);
udi = Udi.Parse("umb://any-guid/00000000000000000000000000000000");
Assert.IsTrue(udi.IsRoot);
Assert.IsInstanceOf<GuidUdi>(udi);
udi = Udi.Parse("umb://any-guid/");
Assert.IsTrue(udi.IsRoot);
Assert.IsInstanceOf<GuidUdi>(udi);
}
[Test]
@@ -222,5 +252,112 @@ namespace Umbraco.Tests.CoreThings
Assert.AreEqual(string.Format("umb://any-guid/{0:N}", guid), drange.Udi.UriValue.ToString());
Assert.AreEqual(Constants.DeploySelector.ChildrenOfThis, drange.Selector);
}
[Test]
public void ValidateUdiEntityType()
{
var types = Constants.UdiEntityType.GetTypes();
foreach (var fi in typeof(Constants.UdiEntityType).GetFields(BindingFlags.Public | BindingFlags.Static))
{
// IsLiteral determines if its value is written at
// compile time and not changeable
// IsInitOnly determine if the field can be set
// in the body of the constructor
// for C# a field which is readonly keyword would have both true
// but a const field would have only IsLiteral equal to true
if (fi.IsLiteral && fi.IsInitOnly == false)
{
var value = fi.GetValue(null).ToString();
if (types.ContainsKey(value) == false)
Assert.Fail("Error in class Constants.UdiEntityType, type \"{0}\" is not declared by GetTypes.", value);
types.Remove(value);
}
}
Assert.AreEqual(0, types.Count, "Error in class Constants.UdiEntityType, GetTypes declares types that don't exist ({0}).", string.Join(",", types.Keys.Select(x => "\"" + x + "\"")));
}
[Test]
public void KnownTypes()
{
Udi udi;
// cannot parse an unknown type, udi is null
// this will scan
Assert.IsFalse(Udi.TryParse("umb://whatever/1234", out udi));
Assert.IsNull(udi);
Udi.ResetUdiTypes();
// unless we want to know
Assert.IsFalse(Udi.TryParse("umb://whatever/1234", true, out udi));
Assert.AreEqual(Constants.UdiEntityType.Unknown, udi.EntityType);
Assert.AreEqual("Umbraco.Core.Udi+UnknownTypeUdi", udi.GetType().FullName);
Udi.ResetUdiTypes();
// not known
Assert.IsFalse(Udi.TryParse("umb://foo/A87F65C8D6B94E868F6949BA92C93045", true, out udi));
Assert.AreEqual(Constants.UdiEntityType.Unknown, udi.EntityType);
Assert.AreEqual("Umbraco.Core.Udi+UnknownTypeUdi", udi.GetType().FullName);
// scanned
Assert.IsTrue(Udi.TryParse("umb://foo/A87F65C8D6B94E868F6949BA92C93045", out udi));
Assert.IsInstanceOf<GuidUdi>(udi);
// known
Assert.IsTrue(Udi.TryParse("umb://foo/A87F65C8D6B94E868F6949BA92C93045", true, out udi));
Assert.IsInstanceOf<GuidUdi>(udi);
// can get method for Deploy compatibility
var method = typeof(Udi).GetMethod("Parse", BindingFlags.Static | BindingFlags.Public, null, new[] { typeof(string), typeof(bool) }, null);
Assert.IsNotNull(method);
}
[UdiDefinition("foo", UdiType.GuidUdi)]
public class FooConnector : IServiceConnector
{
public IArtifact GetArtifact(Udi udi)
{
throw new NotImplementedException();
}
public IArtifact GetArtifact(object entity)
{
throw new NotImplementedException();
}
public ArtifactDeployState ProcessInit(IArtifact art, IDeployContext context)
{
throw new NotImplementedException();
}
public void Process(ArtifactDeployState dart, IDeployContext context, int pass)
{
throw new NotImplementedException();
}
public void Explode(UdiRange range, List<Udi> udis)
{
throw new NotImplementedException();
}
public NamedUdiRange GetRange(Udi udi, string selector)
{
throw new NotImplementedException();
}
public NamedUdiRange GetRange(string entityType, string sid, string selector)
{
throw new NotImplementedException();
}
public bool Compare(IArtifact art1, IArtifact art2, ICollection<Difference> differences = null)
{
throw new NotImplementedException();
}
}
}
}
@@ -21,6 +21,21 @@ namespace Umbraco.Tests.FrontEnd
Assert.AreEqual("Hello world, this is some&hellip;", result);
}
/// <summary>
/// If a truncated string ends with a space, we should trim the space before appending the ellipsis.
/// </summary>
[Test]
public void Truncate_Simple_With_Trimming()
{
var text = "Hello world, this is some text <a href='blah'>with a link</a>";
var helper = new UmbracoHelper();
var result = helper.Truncate(text, 26).ToString();
Assert.AreEqual("Hello world, this is some&hellip;", result);
}
[Test]
public void Truncate_Inside_Word()
{
@@ -78,5 +93,79 @@ namespace Umbraco.Tests.FrontEnd
Assert.AreEqual(expectedResult, result);
}
[Test]
public void Truncate_By_Words()
{
var text = "Hello world, this is some text <a href='blah'>with a link</a>";
var helper = new UmbracoHelper();
var result = helper.TruncateByWords(text, 4).ToString();
Assert.AreEqual("Hello world, this is&hellip;", result);
}
[Test]
public void Truncate_By_Words_With_Tag()
{
var text = "Hello world, <b>this</b> is some text <a href='blah'>with a link</a>";
var helper = new UmbracoHelper();
var result = helper.TruncateByWords(text, 4).ToString();
Assert.AreEqual("Hello world, <b>this</b> is&hellip;", result);
}
[Test]
public void Truncate_By_Words_Mid_Tag()
{
var text = "Hello world, this is some text <a href='blah'>with a link</a>";
var helper = new UmbracoHelper();
var result = helper.TruncateByWords(text, 7).ToString();
Assert.AreEqual("Hello world, this is some text <a href='blah'>with&hellip;</a>", result);
}
[Test]
public void Strip_All_Html()
{
var text = "Hello world, <b>this</b> is some text <a href='blah'>with a link</a>";
var helper = new UmbracoHelper();
var result = helper.StripHtml(text, null).ToString();
Assert.AreEqual("Hello world, this is some text with a link", result);
}
[Test]
public void Strip_Specific_Html()
{
var text = "Hello world, <b>this</b> is some text <a href='blah'>with a link</a>";
string [] tags = {"b"};
var helper = new UmbracoHelper();
var result = helper.StripHtml(text, tags).ToString();
Assert.AreEqual("Hello world, this is some text <a href='blah'>with a link</a>", result);
}
[Test]
public void Strip_Invalid_Html()
{
var text = "Hello world, <bthis</b> is some text <a href='blah'>with a link</a>";
var helper = new UmbracoHelper();
var result = helper.StripHtml(text).ToString();
Assert.AreEqual("Hello world, is some text with a link", result);
}
}
}
@@ -2,7 +2,7 @@
using NUnit.Framework;
using Umbraco.Core;
namespace Umbraco.Tests
namespace Umbraco.Tests.Misc
{
[TestFixture]
public class DateTimeExtensionsTests
@@ -0,0 +1,184 @@
using System;
using System.IO;
using System.Reflection;
using NUnit.Framework;
using Umbraco.Core;
namespace Umbraco.Tests.Misc
{
[TestFixture]
public class HashGeneratorTests
{
private string Generate(bool isCaseSensitive, params string[] strs)
{
using (var generator = new HashGenerator())
{
foreach (var str in strs)
{
if (isCaseSensitive)
generator.AddString(str);
else
generator.AddCaseInsensitiveString(str);
}
return generator.GenerateHash();
}
}
[Test]
public void Generate_Hash_Multiple_Strings_Case_Sensitive()
{
var hash1 = Generate(true, "hello", "world");
var hash2 = Generate(true, "hello", "world");
var hashFalse1 = Generate(true, "hello", "worlD");
var hashFalse2 = Generate(true, "hEllo", "world");
Assert.AreEqual(hash1, hash2);
Assert.AreNotEqual(hash1, hashFalse1);
Assert.AreNotEqual(hash1, hashFalse2);
}
[Test]
public void Generate_Hash_Multiple_Strings_Case_Insensitive()
{
var hash1 = Generate(false, "hello", "world");
var hash2 = Generate(false, "hello", "world");
var hashFalse1 = Generate(false, "hello", "worlD");
var hashFalse2 = Generate(false, "hEllo", "world");
Assert.AreEqual(hash1, hash2);
Assert.AreEqual(hash1, hashFalse1);
Assert.AreEqual(hash1, hashFalse2);
}
private DirectoryInfo PrepareFolder()
{
var assDir = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory;
var dir = Directory.CreateDirectory(Path.Combine(assDir.FullName, "HashCombiner", Guid.NewGuid().ToString("N")));
foreach (var f in dir.GetFiles())
{
f.Delete();
}
return dir;
}
[Test]
public void HashCombiner_Test_String()
{
using (var combiner1 = new HashGenerator())
using (var combiner2 = new HashGenerator())
{
combiner1.AddCaseInsensitiveString("Hello");
combiner2.AddCaseInsensitiveString("hello");
Assert.AreEqual(combiner1.GenerateHash(), combiner2.GenerateHash());
combiner2.AddCaseInsensitiveString("world");
Assert.AreNotEqual(combiner1.GenerateHash(), combiner2.GenerateHash());
}
}
[Test]
public void HashCombiner_Test_Int()
{
using (var combiner1 = new HashGenerator())
using (var combiner2 = new HashGenerator())
{
combiner1.AddInt(1234);
combiner2.AddInt(1234);
Assert.AreEqual(combiner1.GenerateHash(), combiner2.GenerateHash());
combiner2.AddInt(1);
Assert.AreNotEqual(combiner1.GenerateHash(), combiner2.GenerateHash());
}
}
[Test]
public void HashCombiner_Test_DateTime()
{
using (var combiner1 = new HashGenerator())
using (var combiner2 = new HashGenerator())
{
var dt = DateTime.Now;
combiner1.AddDateTime(dt);
combiner2.AddDateTime(dt);
Assert.AreEqual(combiner1.GenerateHash(), combiner2.GenerateHash());
combiner2.AddDateTime(DateTime.Now);
Assert.AreNotEqual(combiner1.GenerateHash(), combiner2.GenerateHash());
}
}
[Test]
public void HashCombiner_Test_File()
{
using (var combiner1 = new HashGenerator())
using (var combiner2 = new HashGenerator())
using (var combiner3 = new HashGenerator())
{
var dir = PrepareFolder();
var file1Path = Path.Combine(dir.FullName, "hastest1.txt");
File.Delete(file1Path);
using (var file1 = File.CreateText(Path.Combine(dir.FullName, "hastest1.txt")))
{
file1.WriteLine("hello");
}
var file2Path = Path.Combine(dir.FullName, "hastest2.txt");
File.Delete(file2Path);
using (var file2 = File.CreateText(Path.Combine(dir.FullName, "hastest2.txt")))
{
//even though files are the same, the dates are different
file2.WriteLine("hello");
}
combiner1.AddFile(new FileInfo(file1Path));
combiner2.AddFile(new FileInfo(file1Path));
combiner3.AddFile(new FileInfo(file2Path));
Assert.AreEqual(combiner1.GenerateHash(), combiner2.GenerateHash());
Assert.AreNotEqual(combiner1.GenerateHash(), combiner3.GenerateHash());
combiner2.AddFile(new FileInfo(file2Path));
Assert.AreNotEqual(combiner1.GenerateHash(), combiner2.GenerateHash());
}
}
[Test]
public void HashCombiner_Test_Folder()
{
using (var combiner1 = new HashGenerator())
using (var combiner2 = new HashGenerator())
using (var combiner3 = new HashGenerator())
{
var dir = PrepareFolder();
var file1Path = Path.Combine(dir.FullName, "hastest1.txt");
File.Delete(file1Path);
using (var file1 = File.CreateText(Path.Combine(dir.FullName, "hastest1.txt")))
{
file1.WriteLine("hello");
}
//first test the whole folder
combiner1.AddFolder(dir);
combiner2.AddFolder(dir);
Assert.AreEqual(combiner1.GenerateHash(), combiner2.GenerateHash());
//now add a file to the folder
var file2Path = Path.Combine(dir.FullName, "hastest2.txt");
File.Delete(file2Path);
using (var file2 = File.CreateText(Path.Combine(dir.FullName, "hastest2.txt")))
{
//even though files are the same, the dates are different
file2.WriteLine("hello");
}
combiner3.AddFolder(dir);
Assert.AreNotEqual(combiner1.GenerateHash(), combiner3.GenerateHash());
}
}
}
}
@@ -54,6 +54,9 @@ namespace Umbraco.Tests.Models
[TestCase("3,8", "2,6", "3,2")] // exclude bin
[TestCase("", "6", "")] // exclude bin
[TestCase("1,-1", "1", "1")] // was an issue
[TestCase("-1,1", "1", "1")] // was an issue
public void CombineStartNodes(string groupSn, string userSn, string expected)
{
// 1
@@ -2,9 +2,11 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Web;
using Moq;
using NUnit.Framework;
using Umbraco.Core;
using Umbraco.Core.Cache;
using Umbraco.Core.Configuration.UmbracoSettings;
using Umbraco.Core.IO;
using Umbraco.Core.Models;
@@ -41,29 +43,82 @@ namespace Umbraco.Tests.Persistence.Repositories
base.TearDown();
}
private ContentRepository CreateRepository(IScopeUnitOfWork unitOfWork, out ContentTypeRepository contentTypeRepository, out DataTypeDefinitionRepository dtdRepository)
private ContentRepository CreateRepository(IScopeUnitOfWork unitOfWork, out ContentTypeRepository contentTypeRepository, out DataTypeDefinitionRepository dtdRepository, CacheHelper cacheHelper = null)
{
cacheHelper = cacheHelper ?? CacheHelper;
TemplateRepository tr;
var ctRepository = CreateRepository(unitOfWork, out contentTypeRepository, out tr);
dtdRepository = new DataTypeDefinitionRepository(unitOfWork, CacheHelper, Logger, contentTypeRepository);
dtdRepository = new DataTypeDefinitionRepository(unitOfWork, cacheHelper, Logger, contentTypeRepository);
return ctRepository;
}
private ContentRepository CreateRepository(IScopeUnitOfWork unitOfWork, out ContentTypeRepository contentTypeRepository)
private ContentRepository CreateRepository(IScopeUnitOfWork unitOfWork, out ContentTypeRepository contentTypeRepository, CacheHelper cacheHelper = null)
{
TemplateRepository tr;
return CreateRepository(unitOfWork, out contentTypeRepository, out tr);
return CreateRepository(unitOfWork, out contentTypeRepository, out tr, cacheHelper);
}
private ContentRepository CreateRepository(IScopeUnitOfWork unitOfWork, out ContentTypeRepository contentTypeRepository, out TemplateRepository templateRepository)
private ContentRepository CreateRepository(IScopeUnitOfWork unitOfWork, out ContentTypeRepository contentTypeRepository, out TemplateRepository templateRepository, CacheHelper cacheHelper = null)
{
templateRepository = new TemplateRepository(unitOfWork, CacheHelper, Logger, Mock.Of<IFileSystem>(), Mock.Of<IFileSystem>(), Mock.Of<ITemplatesSection>());
var tagRepository = new TagRepository(unitOfWork, CacheHelper, Logger);
contentTypeRepository = new ContentTypeRepository(unitOfWork, CacheHelper, Logger, templateRepository);
var repository = new ContentRepository(unitOfWork, CacheHelper, Logger, contentTypeRepository, templateRepository, tagRepository, Mock.Of<IContentSection>());
cacheHelper = cacheHelper ?? CacheHelper;
templateRepository = new TemplateRepository(unitOfWork, cacheHelper, Logger, Mock.Of<IFileSystem>(), Mock.Of<IFileSystem>(), Mock.Of<ITemplatesSection>());
var tagRepository = new TagRepository(unitOfWork, cacheHelper, Logger);
contentTypeRepository = new ContentTypeRepository(unitOfWork, cacheHelper, Logger, templateRepository);
var repository = new ContentRepository(unitOfWork, cacheHelper, Logger, contentTypeRepository, templateRepository, tagRepository, Mock.Of<IContentSection>());
return repository;
}
[Test]
public void Cache_Active_By_Int_And_Guid()
{
ContentTypeRepository contentTypeRepository;
var realCache = new CacheHelper(
new ObjectCacheRuntimeCacheProvider(),
new StaticCacheProvider(),
new StaticCacheProvider(),
new IsolatedRuntimeCache(t => new ObjectCacheRuntimeCacheProvider()));
var provider = TestObjects.GetScopeUnitOfWorkProvider(Logger);
using (var unitOfWork = provider.CreateUnitOfWork())
{
var repository = CreateRepository(unitOfWork, out contentTypeRepository, cacheHelper: realCache);
var udb = (UmbracoDatabase) unitOfWork.Database;
udb.EnableSqlCount = false;
var contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage1", "Textpage");
var content = MockedContent.CreateSimpleContent(contentType);
contentTypeRepository.AddOrUpdate(contentType);
repository.AddOrUpdate(content);
unitOfWork.Complete();
udb.EnableSqlCount = true;
//go get it, this should already be cached since the default repository key is the INT
var found = repository.Get(content.Id);
Assert.AreEqual(0, udb.SqlCount);
//retrieve again, this should use cache
found = repository.Get(content.Id);
Assert.AreEqual(0, udb.SqlCount);
//reset counter
udb.EnableSqlCount = false;
udb.EnableSqlCount = true;
//now get by GUID, this won't be cached yet because the default repo key is not a GUID
found = repository.Get(content.Key);
var sqlCount = udb.SqlCount;
Assert.Greater(sqlCount, 0);
//retrieve again, this should use cache now
found = repository.Get(content.Key);
Assert.AreEqual(sqlCount, udb.SqlCount);
}
}
[Test]
public void Get_Always_Returns_Latest_Version()
{
@@ -822,6 +877,16 @@ namespace Umbraco.Tests.Persistence.Repositories
Assert.That(contents, Is.Not.Null);
Assert.That(contents.Any(), Is.True);
Assert.That(contents.Count(), Is.GreaterThanOrEqualTo(4));
contents = repository.GetAll(contents.Select(x => x.Id).ToArray());
Assert.That(contents, Is.Not.Null);
Assert.That(contents.Any(), Is.True);
Assert.That(contents.Count(), Is.GreaterThanOrEqualTo(4));
contents = ((IReadRepository<Guid, IContent>)repository).GetAll(contents.Select(x => x.Key).ToArray());
Assert.That(contents, Is.Not.Null);
Assert.That(contents.Any(), Is.True);
Assert.That(contents.Count(), Is.GreaterThanOrEqualTo(4));
}
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using Umbraco.Core.Models;
@@ -148,7 +149,6 @@ namespace Umbraco.Tests.Persistence.Repositories
}
[Test]
public void Can_Perform_GetAll_On_DictionaryRepository()
{
@@ -354,6 +354,24 @@ namespace Umbraco.Tests.Persistence.Repositories
}
}
[Test]
public void Can_Perform_GetDictionaryItemKeyMap_On_DictionaryRepository()
{
Dictionary<string, Guid> keyMap;
var provider = TestObjects.GetScopeUnitOfWorkProvider(Logger);
using (var unitOfWork = provider.CreateUnitOfWork())
{
var repository = CreateRepository(unitOfWork);
keyMap = repository.GetDictionaryItemKeyMap();
}
Assert.IsNotNull(keyMap);
Assert.IsNotEmpty(keyMap);
foreach (var kvp in keyMap)
Console.WriteLine("{0}: {1}", kvp.Key, kvp.Value);
}
[TearDown]
public override void TearDown()
{
@@ -2,10 +2,12 @@
using System.Linq;
using Moq;
using NUnit.Framework;
using Umbraco.Core.Cache;
using Umbraco.Core.Configuration.UmbracoSettings;
using Umbraco.Core.Models;
using Umbraco.Core.Models.EntityBase;
using Umbraco.Core.Models.Rdbms;
using Umbraco.Core.Persistence;
using Umbraco.Core.Persistence.Repositories;
using Umbraco.Core.Persistence.UnitOfWork;
using Umbraco.Tests.TestHelpers;
@@ -26,14 +28,65 @@ namespace Umbraco.Tests.Persistence.Repositories
CreateTestData();
}
private MediaRepository CreateRepository(IScopeUnitOfWork unitOfWork, out MediaTypeRepository mediaTypeRepository)
private MediaRepository CreateRepository(IScopeUnitOfWork unitOfWork, out MediaTypeRepository mediaTypeRepository, CacheHelper cacheHelper = null)
{
mediaTypeRepository = new MediaTypeRepository(unitOfWork, CacheHelper, Logger);
var tagRepository = new TagRepository(unitOfWork, CacheHelper, Logger);
var repository = new MediaRepository(unitOfWork, CacheHelper, Logger, mediaTypeRepository, tagRepository, Mock.Of<IContentSection>());
cacheHelper = cacheHelper ?? CacheHelper;
mediaTypeRepository = new MediaTypeRepository(unitOfWork, cacheHelper, Logger);
var tagRepository = new TagRepository(unitOfWork, cacheHelper, Logger);
var repository = new MediaRepository(unitOfWork, cacheHelper, Logger, mediaTypeRepository, tagRepository, Mock.Of<IContentSection>());
return repository;
}
[Test]
public void Cache_Active_By_Int_And_Guid()
{
MediaTypeRepository mediaTypeRepository;
var realCache = new CacheHelper(
new ObjectCacheRuntimeCacheProvider(),
new StaticCacheProvider(),
new StaticCacheProvider(),
new IsolatedRuntimeCache(t => new ObjectCacheRuntimeCacheProvider()));
var provider = TestObjects.GetScopeUnitOfWorkProvider(Logger);
using (var unitOfWork = provider.CreateUnitOfWork())
{
var repository = CreateRepository(unitOfWork, out mediaTypeRepository, cacheHelper: realCache);
var udb = (UmbracoDatabase)unitOfWork.Database;
udb.EnableSqlCount = false;
var mediaType = MockedContentTypes.CreateSimpleMediaType("umbTextpage1", "Textpage");
var media = MockedMedia.CreateSimpleMedia(mediaType, "hello", -1);
mediaTypeRepository.AddOrUpdate(mediaType);
repository.AddOrUpdate(media);
unitOfWork.Complete();
udb.EnableSqlCount = true;
//go get it, this should already be cached since the default repository key is the INT
var found = repository.Get(media.Id);
Assert.AreEqual(0, udb.SqlCount);
//retrieve again, this should use cache
found = repository.Get(media.Id);
Assert.AreEqual(0, udb.SqlCount);
//reset counter
udb.EnableSqlCount = false;
udb.EnableSqlCount = true;
//now get by GUID, this won't be cached yet because the default repo key is not a GUID
found = repository.Get(media.Key);
var sqlCount = udb.SqlCount;
Assert.Greater(sqlCount, 0);
//retrieve again, this should use cache now
found = repository.Get(media.Key);
Assert.AreEqual(sqlCount, udb.SqlCount);
}
}
[Test]
public void Can_Perform_Add_On_MediaRepository()
{
@@ -486,6 +539,16 @@ namespace Umbraco.Tests.Persistence.Repositories
Assert.That(medias, Is.Not.Null);
Assert.That(medias.Any(), Is.True);
Assert.That(medias.Count(), Is.GreaterThanOrEqualTo(3));
medias = repository.GetAll(medias.Select(x => x.Id).ToArray());
Assert.That(medias, Is.Not.Null);
Assert.That(medias.Any(), Is.True);
Assert.That(medias.Count(), Is.GreaterThanOrEqualTo(3));
medias = ((IReadRepository<Guid, IMedia>)repository).GetAll(medias.Select(x => x.Key).ToArray());
Assert.That(medias, Is.Not.Null);
Assert.That(medias.Any(), Is.True);
Assert.That(medias.Count(), Is.GreaterThanOrEqualTo(3));
}
}
@@ -0,0 +1,94 @@
using System.Linq;
using NUnit.Framework;
using Umbraco.Core.Persistence.Repositories;
namespace Umbraco.Tests.Persistence.Repositories
{
[TestFixture]
public class SimilarNodeNameTests
{
[TestCase("Alpha", "Alpha", 0)]
[TestCase("Alpha", "ALPHA", +1)] // case is important
[TestCase("Alpha", "Bravo", -1)]
[TestCase("Bravo", "Alpha", +1)]
[TestCase("Alpha (1)", "Alpha (1)", 0)]
[TestCase("Alpha", "Alpha (1)", -1)]
[TestCase("Alpha (1)", "Alpha", +1)]
[TestCase("Alpha (1)", "Alpha (2)", -1)]
[TestCase("Alpha (2)", "Alpha (1)", +1)]
[TestCase("Alpha (2)", "Alpha (10)", -1)] // this is the real stuff
[TestCase("Alpha (10)", "Alpha (2)", +1)] // this is the real stuff
[TestCase("Kilo", "Golf (2)", +1)]
[TestCase("Kilo (1)", "Golf (2)", +1)]
public void ComparerTest(string name1, string name2, int expected)
{
var comparer = new SimilarNodeName.Comparer();
var result = comparer.Compare(new SimilarNodeName { Name = name1 }, new SimilarNodeName { Name = name2 });
if (expected == 0)
Assert.AreEqual(0, result);
else if (expected < 0)
Assert.IsTrue(result < 0, "Expected <0 but was " + result);
else if (expected > 0)
Assert.IsTrue(result > 0, "Expected >0 but was " + result);
}
[Test]
public void OrderByTest()
{
var names = new[]
{
new SimilarNodeName { Id = 1, Name = "Alpha (2)" },
new SimilarNodeName { Id = 2, Name = "Alpha" },
new SimilarNodeName { Id = 3, Name = "Golf" },
new SimilarNodeName { Id = 4, Name = "Zulu" },
new SimilarNodeName { Id = 5, Name = "Mike" },
new SimilarNodeName { Id = 6, Name = "Kilo (1)" },
new SimilarNodeName { Id = 7, Name = "Yankee" },
new SimilarNodeName { Id = 8, Name = "Kilo" },
new SimilarNodeName { Id = 9, Name = "Golf (2)" },
new SimilarNodeName { Id = 10, Name = "Alpha (1)" },
};
var ordered = names.OrderBy(x => x, new SimilarNodeName.Comparer()).ToArray();
var i = 0;
Assert.AreEqual(2, ordered[i++].Id);
Assert.AreEqual(10, ordered[i++].Id);
Assert.AreEqual(1, ordered[i++].Id);
Assert.AreEqual(3, ordered[i++].Id);
Assert.AreEqual(9, ordered[i++].Id);
Assert.AreEqual(8, ordered[i++].Id);
Assert.AreEqual(6, ordered[i++].Id);
Assert.AreEqual(5, ordered[i++].Id);
Assert.AreEqual(7, ordered[i++].Id);
Assert.AreEqual(4, ordered[i++].Id);
}
[TestCase(0, "Charlie", "Charlie")]
[TestCase(0, "Zulu", "Zulu (1)")]
[TestCase(0, "Golf", "Golf (1)")]
[TestCase(0, "Kilo", "Kilo (2)")]
[TestCase(0, "Alpha", "Alpha (3)")]
[TestCase(0, "Kilo (1)", "Kilo (1) (1)")] // though... we might consider "Kilo (2)"
[TestCase(6, "Kilo (1)", "Kilo (1)")] // because of the id
public void Test(int nodeId, string nodeName, string expected)
{
var names = new[]
{
new SimilarNodeName { Id = 1, Name = "Alpha (2)" },
new SimilarNodeName { Id = 2, Name = "Alpha" },
new SimilarNodeName { Id = 3, Name = "Golf" },
new SimilarNodeName { Id = 4, Name = "Zulu" },
new SimilarNodeName { Id = 5, Name = "Mike" },
new SimilarNodeName { Id = 6, Name = "Kilo (1)" },
new SimilarNodeName { Id = 7, Name = "Yankee" },
new SimilarNodeName { Id = 8, Name = "Kilo" },
new SimilarNodeName { Id = 9, Name = "Golf (2)" },
new SimilarNodeName { Id = 10, Name = "Alpha (1)" },
};
Assert.AreEqual(expected, SimilarNodeName.GetUniqueName(names, nodeId, nodeName));
}
}
}
@@ -541,6 +541,7 @@ namespace Umbraco.Tests.Persistence
{
var helper = new DatabaseSchemaHelper(scope.Database, Mock.Of<ILogger>());
helper.CreateTable<NodeDto>();
helper.CreateTable<UserGroupDto>();
scope.Complete();
@@ -204,15 +204,7 @@ AnotherContentFinder
//ensure they are all found
Assert.IsTrue(plugins.Result.ContainsAll(shouldContain));
}
[Test]
public void PluginHash_From_String()
{
var s = "hello my name is someone".GetHashCode().ToString("x", CultureInfo.InvariantCulture);
var output = TypeLoader.ConvertHashToInt64(s);
Assert.AreNotEqual(0, output);
}
[Test]
public void Get_Plugins_Hash()
{
@@ -1,4 +1,7 @@
using NUnit.Framework;
using System;
using System.Globalization;
using NUnit.Framework;
using Umbraco.Core.Models;
using Umbraco.Tests.TestHelpers;
using Umbraco.Tests.Testing;
using Umbraco.Web.Routing;
@@ -65,6 +68,80 @@ namespace Umbraco.Tests.Routing
var result = lookup.TryFindContent(frequest);
Assert.IsTrue(result);
Assert.AreEqual(expectedId, frequest.PublishedContent.Id);
}
/// <summary>
/// This test handles requests with special characters in the URL.
/// </summary>
/// <param name="urlString"></param>
/// <param name="expectedId"></param>
[TestCase("/", 1046)]
[TestCase("/home/sub1/custom-sub-3-with-accént-character", 1179)]
[TestCase("/home/sub1/custom-sub-4-with-æøå", 1180)]
public void Match_Document_By_Url_With_Special_Characters(string urlString, int expectedId)
{
var umbracoContext = GetUmbracoContext(urlString);
var facadeRouter = CreateFacadeRouter();
var frequest = facadeRouter.CreateRequest(umbracoContext);
var lookup = new ContentFinderByNiceUrl(Logger);
SettingsForTests.HideTopLevelNodeFromPath = false;
var result = lookup.TryFindContent(frequest);
Assert.IsTrue(result);
Assert.AreEqual(expectedId, frequest.PublishedContent.Id);
}
/// <summary>
/// This test handles requests with a hostname associated.
/// The logic for handling this goes through the DomainHelper and is a bit different
/// from what happens in a normal request - so it has a separate test with a mocked
/// hostname added.
/// </summary>
/// <param name="urlString"></param>
/// <param name="expectedId"></param>
[TestCase("/", 1046)]
[TestCase("/home/sub1/custom-sub-3-with-accént-character", 1179)]
[TestCase("/home/sub1/custom-sub-4-with-æøå", 1180)]
public void Match_Document_By_Url_With_Special_Characters_Using_Hostname(string urlString, int expectedId)
{
var umbracoContext = GetUmbracoContext(urlString);
var facadeRouter = CreateFacadeRouter();
var frequest = facadeRouter.CreateRequest(umbracoContext);
frequest.Domain = new DomainAndUri(new Domain(1, "mysite", -1, CultureInfo.CurrentCulture, false), new Uri("http://mysite/"));
var lookup = new ContentFinderByNiceUrl(Logger);
SettingsForTests.HideTopLevelNodeFromPath = false;
var result = lookup.TryFindContent(frequest);
Assert.IsTrue(result);
Assert.AreEqual(expectedId, frequest.PublishedContent.Id);
}
/// <summary>
/// This test handles requests with a hostname with special characters associated.
/// The logic for handling this goes through the DomainHelper and is a bit different
/// from what happens in a normal request - so it has a separate test with a mocked
/// hostname added.
/// </summary>
/// <param name="urlString"></param>
/// <param name="expectedId"></param>
[TestCase("/æøå/", 1046)]
[TestCase("/æøå/home/sub1", 1173)]
[TestCase("/æøå/home/sub1/custom-sub-3-with-accént-character", 1179)]
[TestCase("/æøå/home/sub1/custom-sub-4-with-æøå", 1180)]
public void Match_Document_By_Url_With_Special_Characters_In_Hostname(string urlString, int expectedId)
{
var umbracoContext = GetUmbracoContext(urlString);
var facadeRouter = CreateFacadeRouter();
var frequest = facadeRouter.CreateRequest(umbracoContext);
frequest.Domain = new DomainAndUri(new Domain(1, "mysite/æøå", -1, CultureInfo.CurrentCulture, false), new Uri("http://mysite/æøå"));
var lookup = new ContentFinderByNiceUrl(Logger);
SettingsForTests.HideTopLevelNodeFromPath = false;
var result = lookup.TryFindContent(frequest);
Assert.IsTrue(result);
Assert.AreEqual(expectedId, frequest.PublishedContent.Id);
}
@@ -1145,6 +1145,21 @@ namespace Umbraco.Tests.Services
Assert.That(content.Published, Is.True);
}
[Test]
public void IsPublishable()
{
// Arrange
var contentService = ServiceContext.ContentService;
var parent = contentService.CreateContent("parent", -1, "umbTextpage");
contentService.SaveAndPublishWithStatus(parent);
var content = contentService.CreateContent("child", parent, "umbTextpage");
contentService.Save(content);
Assert.IsTrue(contentService.IsPublishable(content));
contentService.UnPublish(parent);
Assert.IsFalse(contentService.IsPublishable(content));
}
[Test]
public void Can_Publish_Content_WithEvents()
{
@@ -1318,6 +1333,39 @@ namespace Umbraco.Tests.Services
Assert.That(content.Published, Is.True);
Assert.That(published, Is.True);
}
/// <summary>
/// Try to immitate a new child content item being created through the UI.
/// This content item will have no Id, Path or Identity.
/// It seems like this is wiped somewhere in the process when creating an item through the UI
/// and we need to make sure we handle nullchecks for these properties when creating content.
/// This is unfortunately not caught by the normal ContentService tests.
/// </summary>
[Test]
public void Can_Save_And_Publish_Content_And_Child_Without_Identity()
{
// Arrange
var contentService = ServiceContext.ContentService;
var content = contentService.CreateContent("Home US", -1, "umbTextpage", 0);
content.SetValue("author", "Barack Obama");
// Act
var published = contentService.SaveAndPublish(content, 0);
var childContent = contentService.CreateContent("Child", content.Id, "umbTextpage", 0);
// Reset all identity properties
childContent.Id = 0;
childContent.Path = null;
((Content)childContent).ResetIdentity();
var childPublished = contentService.SaveAndPublish(childContent, 0);
// Assert
Assert.That(content.HasIdentity, Is.True);
Assert.That(content.Published, Is.True);
Assert.That(childContent.HasIdentity, Is.True);
Assert.That(childContent.Published, Is.True);
Assert.That(published, Is.True);
Assert.That(childPublished, Is.True);
}
[Test]
public void Can_Get_Published_Descendant_Versions()
@@ -1636,6 +1684,14 @@ namespace Umbraco.Tests.Services
ServiceContext.ContentService.Save(content2, 0);
Assert.IsTrue(ServiceContext.ContentService.PublishWithStatus(content2, 0).Success);
var editorGroup = ServiceContext.UserService.GetUserGroupByAlias("editor");
editorGroup.StartContentId = content1.Id;
ServiceContext.UserService.Save(editorGroup);
var admin = ServiceContext.UserService.GetUserById(0);
admin.StartContentIds = new[] {content1.Id};
ServiceContext.UserService.Save(admin);
ServiceContext.RelationService.Save(new RelationType(Constants.ObjectTypes.DocumentGuid, Constants.ObjectTypes.DocumentGuid, "test"));
Assert.IsNotNull(ServiceContext.RelationService.Relate(content1, content2, "test"));
@@ -1661,6 +1717,7 @@ namespace Umbraco.Tests.Services
}).Success);
// Act
ServiceContext.ContentService.MoveToRecycleBin(content1);
ServiceContext.ContentService.EmptyRecycleBin();
var contents = ServiceContext.ContentService.GetContentInRecycleBin();
@@ -536,9 +536,11 @@ namespace Umbraco.Tests.Services
public void EntityService_Cannot_Get_Key_For_Id_With_Incorrect_Object_Type()
{
var service = ServiceContext.EntityService;
var result = service.GetKeyForId(1060, UmbracoObjectTypes.MediaType);
var result1 = service.GetKeyForId(1060, UmbracoObjectTypes.DocumentType);
var result2 = service.GetKeyForId(1060, UmbracoObjectTypes.MediaType);
Assert.IsFalse(result.Success);
Assert.IsTrue(result1.Success);
Assert.IsFalse(result2.Success);
}
[Test]
@@ -555,9 +557,11 @@ namespace Umbraco.Tests.Services
public void EntityService_Cannot_Get_Id_For_Key_With_Incorrect_Object_Type()
{
var service = ServiceContext.EntityService;
var result = service.GetIdForKey(Guid.Parse("1D3A8E6E-2EA9-4CC1-B229-1AEE19821522"), UmbracoObjectTypes.MediaType);
var result1 = service.GetIdForKey(Guid.Parse("1D3A8E6E-2EA9-4CC1-B229-1AEE19821522"), UmbracoObjectTypes.DocumentType);
var result2 = service.GetIdForKey(Guid.Parse("1D3A8E6E-2EA9-4CC1-B229-1AEE19821522"), UmbracoObjectTypes.MediaType);
Assert.IsFalse(result.Success);
Assert.IsTrue(result1.Success);
Assert.IsFalse(result2.Success);
}
private static bool _isSetup = false;
@@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Security;
using NPoco;
using Moq;
using NUnit.Framework;
using Umbraco.Core;
using Umbraco.Core.Composing;
@@ -15,10 +17,12 @@ using Umbraco.Core.Persistence.DatabaseModelDefinitions;
using Umbraco.Core.Persistence.Querying;
using Umbraco.Core.Persistence.Repositories;
using Umbraco.Core.Persistence.UnitOfWork;
using Umbraco.Core.Security;
using Umbraco.Core.Services;
using Umbraco.Tests.TestHelpers;
using Umbraco.Tests.TestHelpers.Entities;
using Umbraco.Tests.Testing;
using Umbraco.Web.Security.Providers;
namespace Umbraco.Tests.Services
{
@@ -26,6 +30,52 @@ namespace Umbraco.Tests.Services
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, FacadeServiceRepositoryEvents = true)]
public class MemberServiceTests : TestWithSomeContentBase
{
public override void SetUp()
{
base.SetUp();
//hack! but we have no choice until we remove the SavePassword method from IMemberService
var providerMock = new Mock<MembersMembershipProvider>(ServiceContext.MemberService) { CallBase = true };
providerMock.Setup(@base => @base.AllowManuallyChangingPassword).Returns(false);
providerMock.Setup(@base => @base.PasswordFormat).Returns(MembershipPasswordFormat.Hashed);
var provider = providerMock.Object;
((MemberService)ServiceContext.MemberService).MembershipProvider = provider;
}
[Test]
public void Can_Set_Password_On_New_Member()
{
IMemberType memberType = MockedContentTypes.CreateSimpleMemberType();
ServiceContext.MemberTypeService.Save(memberType);
//this will construct a member without a password
var member = MockedMember.CreateSimpleMember(memberType, "test", "test@test.com", "test");
ServiceContext.MemberService.Save(member);
Assert.IsTrue(member.RawPasswordValue.StartsWith(Constants.Security.EmptyPasswordPrefix));
ServiceContext.MemberService.SavePassword(member, "hello123456$!");
var foundMember = ServiceContext.MemberService.GetById(member.Id);
Assert.IsNotNull(foundMember);
Assert.AreNotEqual("hello123456$!", foundMember.RawPasswordValue);
Assert.IsFalse(member.RawPasswordValue.StartsWith(Constants.Security.EmptyPasswordPrefix));
}
[Test]
public void Can_Not_Set_Password_On_Existing_Member()
{
IMemberType memberType = MockedContentTypes.CreateSimpleMemberType();
ServiceContext.MemberTypeService.Save(memberType);
//this will construct a member with a password
var member = MockedMember.CreateSimpleMember(memberType, "test", "test@test.com", "hello123456$!", "test");
ServiceContext.MemberService.Save(member);
Assert.IsFalse(member.RawPasswordValue.StartsWith(Constants.Security.EmptyPasswordPrefix));
Assert.Throws<NotSupportedException>(() => ServiceContext.MemberService.SavePassword(member, "HELLO123456$!"));
}
[Test]
public void Can_Create_Member()
{
@@ -6,6 +6,7 @@ using System.Threading;
using NUnit.Framework;
using Umbraco.Core;
using Umbraco.Core.Models;
using Umbraco.Core.Scoping;
using Umbraco.Core.Services;
using Umbraco.Tests.TestHelpers;
using Umbraco.Tests.TestHelpers.Entities;
+22 -22
View File
@@ -193,9 +193,9 @@ namespace Umbraco.Tests.Services
//assert
//there will be 3 since that is how many content items there are
Assert.AreEqual(3, permissions.Count);
//test permissions contains content[0]
Assert.AreEqual(3, permissions.Count);
//test permissions contains content[0]
Assert.IsTrue(permissions.ContainsKey(content[0].Id));
//test that this permissions set contains permissions for all groups
Assert.IsTrue(permissions[content[0].Id].ContainsKey(userGroup1.Id));
@@ -204,9 +204,9 @@ namespace Umbraco.Tests.Services
//test that the correct number of permissions are returned for each group
Assert.AreEqual(2, permissions[content[0].Id][userGroup1.Id].SelectMany(x => x.AssignedPermissions).Count());
Assert.AreEqual(1, permissions[content[0].Id][userGroup2.Id].SelectMany(x => x.AssignedPermissions).Count());
Assert.AreEqual(defaultPermissionCount, permissions[content[0].Id][userGroup3.Id].SelectMany(x => x.AssignedPermissions).Count());
//test permissions contains content[1]
Assert.AreEqual(defaultPermissionCount, permissions[content[0].Id][userGroup3.Id].SelectMany(x => x.AssignedPermissions).Count());
//test permissions contains content[1]
Assert.IsTrue(permissions.ContainsKey(content[1].Id));
//test that this permissions set contains permissions for all groups
Assert.IsTrue(permissions[content[1].Id].ContainsKey(userGroup1.Id));
@@ -332,8 +332,8 @@ namespace Umbraco.Tests.Services
Assert.AreEqual(1, result.EntityId);
allPermissions = result.GetAllPermissions().ToArray();
Assert.AreEqual(5, allPermissions.Length, string.Join(",", allPermissions));
Assert.IsTrue(allPermissions.ContainsAll(new[] { "S", "D", "F", "G", "K" }));
Assert.IsTrue(allPermissions.ContainsAll(new[] { "S", "D", "F", "G", "K" }));
}
[Test]
@@ -389,7 +389,7 @@ namespace Umbraco.Tests.Services
Assert.IsTrue(result.IsDefaultPermissions);
Assert.IsTrue(result.AssignedPermissions.ContainsAll(defaults));
Assert.AreEqual(3, result.EntityId);
Assert.AreEqual(9876, result.UserGroupId);
Assert.AreEqual(9876, result.UserGroupId);
}
[Test]
@@ -406,8 +406,8 @@ namespace Umbraco.Tests.Services
var child1 = MockedContent.CreateSimpleContent(contentType, "child1", parent);
ServiceContext.ContentService.Save(child1);
var child2 = MockedContent.CreateSimpleContent(contentType, "child2", child1);
ServiceContext.ContentService.Save(child2);
ServiceContext.ContentService.Save(child2);
ServiceContext.ContentService.AssignContentPermission(parent, ActionBrowse.Instance.Letter, new int[] { userGroup.Id });
ServiceContext.ContentService.AssignContentPermission(parent, ActionDelete.Instance.Letter, new int[] { userGroup.Id });
ServiceContext.ContentService.AssignContentPermission(parent, ActionMove.Instance.Letter, new int[] { userGroup.Id });
@@ -425,8 +425,8 @@ namespace Umbraco.Tests.Services
[Test]
public void Can_Delete_User()
{
var user = ServiceContext.UserService.CreateUserWithIdentity("JohnDoe", "john@umbraco.io");
var user = ServiceContext.UserService.CreateUserWithIdentity("JohnDoe", "john@umbraco.io");
ServiceContext.UserService.Delete(user, true);
var deleted = ServiceContext.UserService.GetUserById(user.Id);
@@ -570,7 +570,7 @@ namespace Umbraco.Tests.Services
[Test]
public void Get_All_Paged_Users_With_Filter()
{
var users = MockedUser.CreateMulipleUsers(10).ToArray();
var users = MockedUser.CreateMulipleUsers(10).ToArray();
ServiceContext.UserService.Save(users);
var found = ServiceContext.UserService.GetAll(0, 2, out var totalRecs, "username", Direction.Ascending, filter: "test");
@@ -596,7 +596,7 @@ namespace Umbraco.Tests.Services
ServiceContext.UserService.Save(users);
long totalRecs;
var found = ServiceContext.UserService.GetAll(0, 2, out totalRecs, "username", Direction.Ascending, userGroups: new[] { userGroup.Alias });
var found = ServiceContext.UserService.GetAll(0, 2, out totalRecs, "username", Direction.Ascending, includeUserGroups: new[] {userGroup.Alias});
Assert.AreEqual(2, found.Count());
Assert.AreEqual(5, totalRecs);
@@ -821,15 +821,15 @@ namespace Umbraco.Tests.Services
userGroup2.AddAllowedSection("test");
var userGroup3 = new UserGroup
{
{
Alias = "Group3",
Name = "Group 3"
};
ServiceContext.UserService.Save(userGroup1);
ServiceContext.UserService.Save(userGroup2);
ServiceContext.UserService.Save(userGroup3);
//assert
ServiceContext.UserService.Save(userGroup3);
//assert
var result1 = ServiceContext.UserService.GetUserGroupById(userGroup1.Id);
var result2 = ServiceContext.UserService.GetUserGroupById(userGroup2.Id);
var result3 = ServiceContext.UserService.GetUserGroupById(userGroup3.Id);
@@ -857,9 +857,9 @@ namespace Umbraco.Tests.Services
public void Cannot_Create_User_With_Empty_Username()
{
// Arrange
var userService = ServiceContext.UserService;
// Act & Assert
var userService = ServiceContext.UserService;
// Act & Assert
Assert.Throws<ArgumentException>(() => userService.CreateUserWithIdentity(string.Empty, "john@umbraco.io"));
}
@@ -27,7 +27,7 @@ namespace Umbraco.Tests.Strings
var helper = Current.ShortStringHelper;
Assert.IsInstanceOf<MockShortStringHelper>(helper);
}
[TestCase("hello", "world", false)]
[TestCase("hello", "hello", true)]
[TestCase("hellohellohellohellohellohellohello", "hellohellohellohellohellohellohelloo", false)]
@@ -53,6 +53,8 @@ namespace Umbraco.Tests.TestHelpers
</Home>
<CustomDocument id=""1177"" parentID=""1173"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1234"" template=""" + templateId + @""" sortOrder=""4"" createDate=""2012-07-16T15:26:59"" updateDate=""2012-07-18T14:23:35"" nodeName=""custom sub 1"" urlName=""custom-sub-1"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1177"" isDoc="""" />
<CustomDocument id=""1178"" parentID=""1173"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1234"" template=""" + templateId + @""" sortOrder=""4"" createDate=""2012-07-16T15:26:59"" updateDate=""2012-07-16T14:23:35"" nodeName=""custom sub 2"" urlName=""custom-sub-2"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1178"" isDoc="""" />
<CustomDocument id=""1179"" parentID=""1173"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1234"" template=""" + templateId + @""" sortOrder=""4"" createDate=""2012-07-16T15:26:59"" updateDate=""2012-07-16T14:23:35"" nodeName=""custom sub 3 with accént character"" urlName=""custom-sub-3-with-accént-character"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1179"" isDoc="""" />
<CustomDocument id=""1180"" parentID=""1173"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1234"" template=""" + templateId + @""" sortOrder=""4"" createDate=""2012-07-16T15:26:59"" updateDate=""2012-07-16T14:23:35"" nodeName=""custom sub 4 with æøå"" urlName=""custom-sub-4-with-æøå"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1180"" isDoc="""" />
</Home>
<Home id=""1175"" parentID=""1046"" level=""2"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""3"" createDate=""2012-07-20T18:08:01"" updateDate=""2012-07-20T18:49:32"" nodeName=""Sub 2"" urlName=""sub-2"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1175"" isDoc=""""><content><![CDATA[]]></content>
</Home>
@@ -1,5 +1,6 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Web;
@@ -43,6 +44,9 @@ namespace Umbraco.Tests.TestHelpers.ControllerTesting
var owinContext = request.TryGetOwinContext().Result;
var mockedUserService = Mock.Of<IUserService>();
var mockedContentService = Mock.Of<IContentService>();
var mockedMediaService = Mock.Of<IMediaService>();
var mockedEntityService = Mock.Of<IEntityService>();
var mockedMigrationService = new Mock<IMigrationEntryService>();
//set it up to return anything so that the app ctx is 'Configured'
@@ -50,6 +54,9 @@ namespace Umbraco.Tests.TestHelpers.ControllerTesting
var serviceContext = new ServiceContext(
userService: mockedUserService,
contentService: mockedContentService,
mediaService: mockedMediaService,
entityService: mockedEntityService,
migrationEntryService: mockedMigrationService.Object,
localizedTextService:Mock.Of<ILocalizedTextService>(),
sectionService:Mock.Of<ISectionService>());
@@ -86,10 +93,17 @@ namespace Umbraco.Tests.TestHelpers.ControllerTesting
var webSecurity = new Mock<WebSecurity>(null, null);
//mock CurrentUser
var groups = new List<ReadOnlyUserGroup>();
for (var index = 0; index < backofficeIdentity.Roles.Length; index++)
{
var role = backofficeIdentity.Roles[index];
groups.Add(new ReadOnlyUserGroup(index + 1, role, "icon-user", null, null, role, new string[0], new string[0]));
}
webSecurity.Setup(x => x.CurrentUser)
.Returns(Mock.Of<IUser>(u => u.IsApproved == true
&& u.IsLockedOut == false
&& u.AllowedSections == backofficeIdentity.AllowedApplications
&& u.Groups == groups
&& u.Email == "admin@admin.com"
&& u.Id == (int) backofficeIdentity.Id
&& u.Language == "en"
@@ -101,7 +115,7 @@ namespace Umbraco.Tests.TestHelpers.ControllerTesting
//mock Validate
webSecurity.Setup(x => x.ValidateCurrentUser())
.Returns(() => true);
webSecurity.Setup(x => x.UserHasAppAccess(It.IsAny<string>(), It.IsAny<IUser>()))
webSecurity.Setup(x => x.UserHasSectionAccess(It.IsAny<string>(), It.IsAny<IUser>()))
.Returns(() => true);
var umbCtx = UmbracoContext.EnsureContext(
@@ -31,6 +31,29 @@ namespace Umbraco.Tests.TestHelpers.Entities
return member;
}
public static Member CreateSimpleMember(IMemberType contentType, string name, string email, string username, Guid? key = null)
{
var member = new Member(name, email, username, contentType)
{
CreatorId = 0,
Email = email,
Username = username
};
if (key.HasValue)
{
member.Key = key.Value;
}
member.SetValue("title", name + " member");
member.SetValue("bodyText", "This is a subpage");
member.SetValue("author", "John Doe");
member.ResetDirtyProperties(false);
return member;
}
public static IEnumerable<IMember> CreateSimpleMember(IMemberType memberType, int amount, Action<int, IMember> onCreating = null)
{
var list = new List<IMember>();
+2 -2
View File
@@ -157,12 +157,12 @@ namespace Umbraco.Tests.TestHelpers
var userService = GetLazyService<IUserService>(container, () => new UserService(provider, logger, eventMessagesFactory, runtimeState));
var dataTypeService = GetLazyService<IDataTypeService>(container, () => new DataTypeService(provider, logger, eventMessagesFactory));
var contentService = GetLazyService<IContentService>(container, () => new ContentService(provider, logger, eventMessagesFactory, mediaFileSystem, idkMap));
var contentService = GetLazyService<IContentService>(container, () => new ContentService(provider, logger, eventMessagesFactory, mediaFileSystem));
var notificationService = GetLazyService<INotificationService>(container, () => new NotificationService(provider, userService.Value, contentService.Value, logger));
var serverRegistrationService = GetLazyService<IServerRegistrationService>(container, () => new ServerRegistrationService(provider, logger, eventMessagesFactory));
var memberGroupService = GetLazyService<IMemberGroupService>(container, () => new MemberGroupService(provider, logger, eventMessagesFactory));
var memberService = GetLazyService<IMemberService>(container, () => new MemberService(provider, logger, eventMessagesFactory, memberGroupService.Value, mediaFileSystem));
var mediaService = GetLazyService<IMediaService>(container, () => new MediaService(provider, mediaFileSystem, logger, eventMessagesFactory, idkMap));
var mediaService = GetLazyService<IMediaService>(container, () => new MediaService(provider, mediaFileSystem, logger, eventMessagesFactory));
var contentTypeService = GetLazyService<IContentTypeService>(container, () => new ContentTypeService(provider, logger, eventMessagesFactory, contentService.Value));
var mediaTypeService = GetLazyService<IMediaTypeService>(container, () => new MediaTypeService(provider, logger, eventMessagesFactory, mediaService.Value));
var fileService = GetLazyService<IFileService>(container, () => new FileService(provider, logger, eventMessagesFactory));
+6 -1
View File
@@ -218,7 +218,8 @@
<Compile Include="CoreThings\CallContextTests.cs" />
<Compile Include="Components\ComponentTests.cs" />
<Compile Include="CoreXml\RenamedRootNavigatorTests.cs" />
<Compile Include="DateTimeExtensionsTests.cs" />
<Compile Include="Misc\DateTimeExtensionsTests.cs" />
<Compile Include="Misc\HashGeneratorTests.cs" />
<Compile Include="IO\ShadowFileSystemTests.cs" />
<Compile Include="Issues\U9560.cs" />
<Compile Include="Integration\ContentEventsTests.cs" />
@@ -227,6 +228,7 @@
<Compile Include="Models\Mapping\AutoMapper6Tests.cs" />
<Compile Include="Models\Mapping\UserModelMapperTests.cs" />
<Compile Include="Persistence\Repositories\EntityRepositoryTests.cs" />
<Compile Include="Persistence\Repositories\SimilarNodeNameTests.cs" />
<Compile Include="Persistence\Repositories\UserGroupRepositoryTest.cs" />
<Compile Include="PublishedContent\ModelsAndConvertersTests.cs" />
<Compile Include="Routing\NiceUrlRoutesTests.cs" />
@@ -285,8 +287,11 @@
<Compile Include="Collections\DeepCloneableListTests.cs" />
<Compile Include="Web\Controllers\BackOfficeControllerUnitTests.cs" />
<Compile Include="CoreThings\DelegateExtensionsTests.cs" />
<Compile Include="Web\Controllers\UserEditorAuthorizationHelperTests.cs" />
<Compile Include="Web\Controllers\UsersControllerTests.cs" />
<Compile Include="Web\HealthChecks\HealthCheckResultsTests.cs" />
<Compile Include="Web\HttpCookieExtensionsTests.cs" />
<Compile Include="Web\Mvc\HtmlStringUtilitiesTests.cs" />
<Compile Include="Web\Mvc\RenderIndexActionSelectorAttributeTests.cs" />
<Compile Include="Persistence\PetaPocoCachesTest.cs" />
<Compile Include="Persistence\Repositories\AuditRepositoryTest.cs" />
@@ -0,0 +1,427 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Moq;
using NUnit.Framework;
using Umbraco.Core;
using Umbraco.Core.Models;
using Umbraco.Core.Models.EntityBase;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Services;
using Umbraco.Web.Editors;
namespace Umbraco.Tests.Web.Controllers
{
[TestFixture]
public class UserEditorAuthorizationHelperTests
{
[Test]
public void Admin_Is_Authorized()
{
var currentUser = GetAdminUser();
var savingUser = Mock.Of<IUser>();
var contentService = new Mock<IContentService>();
var mediaService = new Mock<IMediaService>();
var userService = new Mock<IUserService>();
var entityService = new Mock<IEntityService>();
var authHelper = new UserEditorAuthorizationHelper(
contentService.Object,
mediaService.Object,
userService.Object,
entityService.Object);
var result = authHelper.IsAuthorized(currentUser, savingUser, new int[0], new int[0], new string[0]);
Assert.IsTrue(result.Success);
}
[Test]
public void Non_Admin_Cannot_Save_Admin()
{
var currentUser = Mock.Of<IUser>();
var savingUser = GetAdminUser();
var contentService = new Mock<IContentService>();
var mediaService = new Mock<IMediaService>();
var userService = new Mock<IUserService>();
var entityService = new Mock<IEntityService>();
var authHelper = new UserEditorAuthorizationHelper(
contentService.Object,
mediaService.Object,
userService.Object,
entityService.Object);
var result = authHelper.IsAuthorized(currentUser, savingUser, new int[0], new int[0], new string[0]);
Assert.IsFalse(result.Success);
}
[Test]
public void Cannot_Grant_Group_Membership_Without_Being_A_Member()
{
var currentUser = Mock.Of<IUser>(user => user.Groups == new[]
{
new ReadOnlyUserGroup(1, "Test", "icon-user", null, null, "test", new string[0], new string[0])
});
var savingUser = Mock.Of<IUser>();
var contentService = new Mock<IContentService>();
var mediaService = new Mock<IMediaService>();
var userService = new Mock<IUserService>();
var entityService = new Mock<IEntityService>();
var authHelper = new UserEditorAuthorizationHelper(
contentService.Object,
mediaService.Object,
userService.Object,
entityService.Object);
var result = authHelper.IsAuthorized(currentUser, savingUser, new int[0], new int[0], new[] {"FunGroup"});
Assert.IsFalse(result.Success);
}
[Test]
public void Can_Grant_Group_Membership_With_Being_A_Member()
{
var currentUser = Mock.Of<IUser>(user => user.Groups == new[]
{
new ReadOnlyUserGroup(1, "Test", "icon-user", null, null, "test", new string[0], new string[0])
});
var savingUser = Mock.Of<IUser>();
var contentService = new Mock<IContentService>();
var mediaService = new Mock<IMediaService>();
var userService = new Mock<IUserService>();
var entityService = new Mock<IEntityService>();
var authHelper = new UserEditorAuthorizationHelper(
contentService.Object,
mediaService.Object,
userService.Object,
entityService.Object);
var result = authHelper.IsAuthorized(currentUser, savingUser, new int[0], new int[0], new[] { "test" });
Assert.IsTrue(result.Success);
}
[Test]
public void Can_Add_Another_Content_Start_Node_On_User_With_Access()
{
var nodePaths = new Dictionary<int, string>
{
{1234, "-1,1234"},
{9876, "-1,9876"},
{5555, "-1,9876,5555"},
{4567, "-1,4567"},
};
var currentUser = Mock.Of<IUser>(user => user.StartContentIds == new[] { 9876 });
var savingUser = Mock.Of<IUser>(user => user.StartContentIds == new[] {1234});
var contentService = new Mock<IContentService>();
contentService.Setup(x => x.GetById(It.IsAny<int>()))
.Returns((int id) => Mock.Of<IContent>(content => content.Path == nodePaths[id]));
var mediaService = new Mock<IMediaService>();
var userService = new Mock<IUserService>();
var entityService = new Mock<IEntityService>();
entityService.Setup(service => service.GetAllPaths(It.IsAny<UmbracoObjectTypes>(), It.IsAny<int[]>()))
.Returns((UmbracoObjectTypes objType, int[] ids) =>
{
return ids.Select(x => new EntityPath {Path = nodePaths[x], Id = x});
});
var authHelper = new UserEditorAuthorizationHelper(
contentService.Object,
mediaService.Object,
userService.Object,
entityService.Object);
//adding 5555 which currentUser has access to since it's a child of 9876 ... adding is still ok even though currentUser doesn't have access to 1234
var result = authHelper.IsAuthorized(currentUser, savingUser, new[] { 1234, 5555 }, new int[0], new string[0]);
Assert.IsTrue(result.Success);
}
[Test]
public void Can_Remove_Content_Start_Node_On_User_Without_Access()
{
var nodePaths = new Dictionary<int, string>
{
{1234, "-1,1234"},
{9876, "-1,9876"},
{5555, "-1,9876,5555"},
{4567, "-1,4567"},
};
var currentUser = Mock.Of<IUser>(user => user.StartContentIds == new[] { 9876 });
var savingUser = Mock.Of<IUser>(user => user.StartContentIds == new[] { 1234, 4567 });
var contentService = new Mock<IContentService>();
contentService.Setup(x => x.GetById(It.IsAny<int>()))
.Returns((int id) => Mock.Of<IContent>(content => content.Path == nodePaths[id]));
var mediaService = new Mock<IMediaService>();
var userService = new Mock<IUserService>();
var entityService = new Mock<IEntityService>();
entityService.Setup(service => service.GetAllPaths(It.IsAny<UmbracoObjectTypes>(), It.IsAny<int[]>()))
.Returns((UmbracoObjectTypes objType, int[] ids) =>
{
return ids.Select(x => new EntityPath { Path = nodePaths[x], Id = x });
});
var authHelper = new UserEditorAuthorizationHelper(
contentService.Object,
mediaService.Object,
userService.Object,
entityService.Object);
//removing 4567 start node even though currentUser doesn't have acces to it ... removing is ok
var result = authHelper.IsAuthorized(currentUser, savingUser, new[] { 1234 }, new int[0], new string[0]);
Assert.IsTrue(result.Success);
}
[Test]
public void Cannot_Add_Content_Start_Node_On_User_Without_Access()
{
var nodePaths = new Dictionary<int, string>
{
{1234, "-1,1234"},
{9876, "-1,9876"},
{5555, "-1,9876,5555"},
{4567, "-1,4567"},
};
var currentUser = Mock.Of<IUser>(user => user.StartContentIds == new[]{9876});
var savingUser = Mock.Of<IUser>();
var contentService = new Mock<IContentService>();
contentService.Setup(x => x.GetById(It.IsAny<int>()))
.Returns((int id) => Mock.Of<IContent>(content => content.Path == nodePaths[id]));
var mediaService = new Mock<IMediaService>();
var userService = new Mock<IUserService>();
var entityService = new Mock<IEntityService>();
entityService.Setup(service => service.GetAllPaths(It.IsAny<UmbracoObjectTypes>(), It.IsAny<int[]>()))
.Returns((UmbracoObjectTypes objType, int[] ids) =>
{
return ids.Select(x => new EntityPath { Path = nodePaths[x], Id = x });
});
var authHelper = new UserEditorAuthorizationHelper(
contentService.Object,
mediaService.Object,
userService.Object,
entityService.Object);
//adding 1234 but currentUser doesn't have access to it ... nope
var result = authHelper.IsAuthorized(currentUser, savingUser, new []{1234}, new int[0], new string[0]);
Assert.IsFalse(result.Success);
}
[Test]
public void Can_Add_Content_Start_Node_On_User_With_Access()
{
var nodePaths = new Dictionary<int, string>
{
{1234, "-1,1234"},
{9876, "-1,9876"},
{5555, "-1,9876,5555"},
{4567, "-1,4567"},
};
var currentUser = Mock.Of<IUser>(user => user.StartContentIds == new[] { 9876 });
var savingUser = Mock.Of<IUser>();
var contentService = new Mock<IContentService>();
contentService.Setup(x => x.GetById(It.IsAny<int>()))
.Returns((int id) => Mock.Of<IContent>(content => content.Path == nodePaths[id]));
var mediaService = new Mock<IMediaService>();
var userService = new Mock<IUserService>();
var entityService = new Mock<IEntityService>();
entityService.Setup(service => service.GetAllPaths(It.IsAny<UmbracoObjectTypes>(), It.IsAny<int[]>()))
.Returns((UmbracoObjectTypes objType, int[] ids) =>
{
return ids.Select(x => new EntityPath { Path = nodePaths[x], Id = x });
});
var authHelper = new UserEditorAuthorizationHelper(
contentService.Object,
mediaService.Object,
userService.Object,
entityService.Object);
//adding 5555 which currentUser has access to since it's a child of 9876 ... ok
var result = authHelper.IsAuthorized(currentUser, savingUser, new[] { 5555 }, new int[0], new string[0]);
Assert.IsTrue(result.Success);
}
[Test]
public void Cannot_Add_Media_Start_Node_On_User_Without_Access()
{
var nodePaths = new Dictionary<int, string>
{
{1234, "-1,1234"},
{9876, "-1,9876"},
{5555, "-1,9876,5555"},
{4567, "-1,4567"},
};
var currentUser = Mock.Of<IUser>(user => user.StartMediaIds == new[] { 9876 });
var savingUser = Mock.Of<IUser>();
var contentService = new Mock<IContentService>();
var mediaService = new Mock<IMediaService>();
mediaService.Setup(x => x.GetById(It.IsAny<int>()))
.Returns((int id) => Mock.Of<IMedia>(content => content.Path == nodePaths[id]));
var userService = new Mock<IUserService>();
var entityService = new Mock<IEntityService>();
entityService.Setup(service => service.GetAllPaths(It.IsAny<UmbracoObjectTypes>(), It.IsAny<int[]>()))
.Returns((UmbracoObjectTypes objType, int[] ids) =>
{
return ids.Select(x => new EntityPath { Path = nodePaths[x], Id = x });
});
var authHelper = new UserEditorAuthorizationHelper(
contentService.Object,
mediaService.Object,
userService.Object,
entityService.Object);
//adding 1234 but currentUser doesn't have access to it ... nope
var result = authHelper.IsAuthorized(currentUser, savingUser, new int[0], new[] {1234}, new string[0]);
Assert.IsFalse(result.Success);
}
[Test]
public void Can_Add_Media_Start_Node_On_User_With_Access()
{
var nodePaths = new Dictionary<int, string>
{
{1234, "-1,1234"},
{9876, "-1,9876"},
{5555, "-1,9876,5555"},
{4567, "-1,4567"},
};
var currentUser = Mock.Of<IUser>(user => user.StartMediaIds == new[] { 9876 });
var savingUser = Mock.Of<IUser>();
var contentService = new Mock<IContentService>();
var mediaService = new Mock<IMediaService>();
mediaService.Setup(x => x.GetById(It.IsAny<int>()))
.Returns((int id) => Mock.Of<IMedia>(content => content.Path == nodePaths[id]));
var userService = new Mock<IUserService>();
var entityService = new Mock<IEntityService>();
entityService.Setup(service => service.GetAllPaths(It.IsAny<UmbracoObjectTypes>(), It.IsAny<int[]>()))
.Returns((UmbracoObjectTypes objType, int[] ids) =>
{
return ids.Select(x => new EntityPath { Path = nodePaths[x], Id = x });
});
var authHelper = new UserEditorAuthorizationHelper(
contentService.Object,
mediaService.Object,
userService.Object,
entityService.Object);
//adding 5555 which currentUser has access to since it's a child of 9876 ... ok
var result = authHelper.IsAuthorized(currentUser, savingUser, new int[0], new[] { 5555 }, new string[0]);
Assert.IsTrue(result.Success);
}
[Test]
public void Can_Add_Another_Media_Start_Node_On_User_With_Access()
{
var nodePaths = new Dictionary<int, string>
{
{1234, "-1,1234"},
{9876, "-1,9876"},
{5555, "-1,9876,5555"},
{4567, "-1,4567"},
};
var currentUser = Mock.Of<IUser>(user => user.StartMediaIds == new[] { 9876 });
var savingUser = Mock.Of<IUser>(user => user.StartMediaIds == new[] { 1234 });
var contentService = new Mock<IContentService>();
var mediaService = new Mock<IMediaService>();
mediaService.Setup(x => x.GetById(It.IsAny<int>()))
.Returns((int id) => Mock.Of<IMedia>(content => content.Path == nodePaths[id]));
var userService = new Mock<IUserService>();
var entityService = new Mock<IEntityService>();
entityService.Setup(service => service.GetAllPaths(It.IsAny<UmbracoObjectTypes>(), It.IsAny<int[]>()))
.Returns((UmbracoObjectTypes objType, int[] ids) =>
{
return ids.Select(x => new EntityPath { Path = nodePaths[x], Id = x });
});
var authHelper = new UserEditorAuthorizationHelper(
contentService.Object,
mediaService.Object,
userService.Object,
entityService.Object);
//adding 5555 which currentUser has access to since it's a child of 9876 ... adding is still ok even though currentUser doesn't have access to 1234
var result = authHelper.IsAuthorized(currentUser, savingUser, new int[0], new[] { 1234, 5555 }, new string[0]);
Assert.IsTrue(result.Success);
}
[Test]
public void Can_Remove_Media_Start_Node_On_User_Without_Access()
{
var nodePaths = new Dictionary<int, string>
{
{1234, "-1,1234"},
{9876, "-1,9876"},
{5555, "-1,9876,5555"},
{4567, "-1,4567"},
};
var currentUser = Mock.Of<IUser>(user => user.StartMediaIds == new[] { 9876 });
var savingUser = Mock.Of<IUser>(user => user.StartMediaIds == new[] { 1234, 4567 });
var contentService = new Mock<IContentService>();
var mediaService = new Mock<IMediaService>();
mediaService.Setup(x => x.GetById(It.IsAny<int>()))
.Returns((int id) => Mock.Of<IMedia>(content => content.Path == nodePaths[id]));
var userService = new Mock<IUserService>();
var entityService = new Mock<IEntityService>();
entityService.Setup(service => service.GetAllPaths(It.IsAny<UmbracoObjectTypes>(), It.IsAny<int[]>()))
.Returns((UmbracoObjectTypes objType, int[] ids) =>
{
return ids.Select(x => new EntityPath { Path = nodePaths[x], Id = x });
});
var authHelper = new UserEditorAuthorizationHelper(
contentService.Object,
mediaService.Object,
userService.Object,
entityService.Object);
//removing 4567 start node even though currentUser doesn't have acces to it ... removing is ok
var result = authHelper.IsAuthorized(currentUser, savingUser, new int[0], new[] { 1234 }, new string[0]);
Assert.IsTrue(result.Success);
}
private IUser GetAdminUser()
{
var admin = Mock.Of<IUser>(user => user.Groups == new[]
{
new ReadOnlyUserGroup(1, "Admin", "icon-user", null, null, Constants.Security.AdminGroupAlias, new string[0], new string[0])
});
return admin;
}
}
}
@@ -12,6 +12,7 @@ using Umbraco.Core.Models;
using Umbraco.Core.Models.Identity;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
using Umbraco.Core.Persistence.Querying;
using Umbraco.Core.Security;
using Umbraco.Tests.TestHelpers;
using Umbraco.Tests.TestHelpers.ControllerTesting;
@@ -107,7 +108,9 @@ namespace Umbraco.Tests.Web.Controllers
var userServiceMock = Mock.Get(Current.Services.UserService);
var users = MockedUser.CreateMulipleUsers(10);
long outVal = 10;
userServiceMock.Setup(service => service.GetAll(It.IsAny<long>(), It.IsAny<int>(), out outVal, It.IsAny<string>(), It.IsAny<Direction>(), It.IsAny<UserState[]>(), It.IsAny<string[]>(), It.IsAny<string>()))
userServiceMock.Setup(service => service.GetAll(
It.IsAny<long>(), It.IsAny<int>(), out outVal, It.IsAny<string>(), It.IsAny<Direction>(),
It.IsAny<UserState[]>(), It.IsAny<string[]>(), It.IsAny<string[]>(), It.IsAny<IQuery<IUser>>()))
.Returns(() => users);
//we need to manually apply automapper mappings with the mocked applicationcontext
@@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using NUnit.Framework;
using Umbraco.Core;
using Umbraco.Web;
namespace Umbraco.Tests.Web
{
[TestFixture]
public class HttpCookieExtensionsTests
{
[TestCase("hello=world;cookies=are fun;", "hello", "world", true)]
[TestCase("HELlo=world;cookies=are fun", "hello", "world", true)]
[TestCase("HELlo= world;cookies=are fun", "hello", "world", true)]
[TestCase("HELlo =world;cookies=are fun", "hello", "world", true)]
[TestCase("hello = world;cookies=are fun;", "hello", "world", true)]
[TestCase("hellos=world;cookies=are fun", "hello", "world", false)]
[TestCase("hello=world;cookies?=are fun?", "hello", "world", true)]
[TestCase("hel?lo=world;cookies=are fun?", "hel?lo", "world", true)]
public void Get_Cookie_Value_From_HttpRequestHeaders(string cookieHeaderVal, string cookieName, string cookieVal, bool matches)
{
var request = new HttpRequestMessage(HttpMethod.Get, "http://test.com");
var requestHeaders = request.Headers;
requestHeaders.Add("Cookie", cookieHeaderVal);
var valueFromHeader = requestHeaders.GetCookieValue(cookieName);
if (matches)
{
Assert.IsNotNull(valueFromHeader);
Assert.AreEqual(cookieVal, valueFromHeader);
}
else
{
Assert.IsNull(valueFromHeader);
}
}
}
}
@@ -0,0 +1,26 @@
using NUnit.Framework;
using Umbraco.Web;
namespace Umbraco.Tests.Web.Mvc
{
[TestFixture]
public class HtmlStringUtilitiesTests
{
private HtmlStringUtilities _htmlStringUtilities;
[SetUp]
public virtual void Initialize()
{
_htmlStringUtilities = new HtmlStringUtilities();
}
[Test]
public void ReplaceLineBreaksWithHtmlBreak()
{
var output = _htmlStringUtilities.ReplaceLineBreaksForHtml("<div><h1>hello world</h1><p>hello world\r\nhello world\rhello world\nhello world</p></div>");
var expected = "<div><h1>hello world</h1><p>hello world<br />hello world<br />hello world<br />hello world</p></div>";
Assert.AreEqual(expected, output);
}
}
}
@@ -62,8 +62,12 @@ namespace Umbraco.Web.Cache
// content and when the PublishedCachesService is notified of changes it does not see
// the new content...
bool draftChanged, publishedChanged;
_facadeService.Notify(payloads, out draftChanged, out publishedChanged);
// fixme - what about this?
// should rename it, and then, this is only for Deploy, and then, ???
//if (Suspendable.PageCacheRefresher.CanUpdateDocumentCache)
// ...
_facadeService.Notify(payloads, out _, out var publishedChanged);
if (payloads.Any(x => x.ChangeTypes.HasType(TreeChangeTypes.RefreshAll)) || publishedChanged)
{
@@ -7,7 +7,7 @@ namespace Umbraco.Web.Cache
{
public sealed class TemplateCacheRefresher : CacheRefresherBase<TemplateCacheRefresher>
{
private readonly IdkMap _idkMap;
private readonly IdkMap _idkMap;
public TemplateCacheRefresher(CacheHelper cacheHelper, IdkMap idkMap)
: base(cacheHelper)
@@ -165,7 +165,7 @@ namespace Umbraco.Web.Editors
? Security.IsAuthenticated()
//current culture is set at the very beginning of each request
? Thread.CurrentThread.CurrentCulture
: CultureInfo.GetCultureInfo("en")
: CultureInfo.GetCultureInfo(GlobalSettings.DefaultUILanguage)
: CultureInfo.GetCultureInfo(culture);
var textForCulture = Services.TextService.GetAllStoredValues(cultureInfo)
@@ -295,10 +295,11 @@ namespace Umbraco.Web.Editors
GetMaxRequestLength()
},
{"keepUserLoggedIn", UmbracoConfig.For.UmbracoSettings().Security.KeepUserLoggedIn},
{"usernameIsEmail", UmbracoConfig.For.UmbracoSettings().Security.UsernameIsEmail},
{"cssPath", IOHelper.ResolveUrl(SystemDirectories.Css).TrimEnd('/')},
{"allowPasswordReset", UmbracoConfig.For.UmbracoSettings().Security.AllowPasswordReset},
{"loginBackgroundImage", UmbracoConfig.For.UmbracoSettings().Content.LoginBackgroundImage},
{"emailServerConfigured", GlobalSettings.HasSmtpServerConfigured(_httpContext.Request.ApplicationPath)},
{"showUserInvite", EmailSender.CanSendRequiredEmail},
}
},
{
+8 -2
View File
@@ -85,6 +85,8 @@ namespace Umbraco.Web.Editors
{
if (saveModel.ContentId <= 0) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound));
//TODO: Should non-admins be alowed to set granular permissions?
var content = Services.ContentService.GetById(saveModel.ContentId);
if (content == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound));
@@ -146,7 +148,9 @@ namespace Umbraco.Web.Editors
if (contentId <= 0) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound));
var content = Services.ContentService.GetById(contentId);
if (content == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound));
//TODO: Should non-admins be able to see detailed permissions?
var allUserGroups = Services.UserService.GetAllUserGroups();
return GetDetailedPermissions(content, allUserGroups);
@@ -232,6 +236,7 @@ namespace Umbraco.Web.Editors
content.Path = string.Format("-1,{0},{1}", persistedContent.ContentTypeId, content.Id);
content.AllowedActions = new[] {"A"};
content.IsBlueprint = true;
var excludeProps = new[] {"_umb_urls", "_umb_releasedate", "_umb_expiredate", "_umb_template"};
var propsTab = content.Tabs.Last();
@@ -299,7 +304,7 @@ namespace Umbraco.Web.Editors
}
[OutgoingEditorModelEvent]
public ContentItemDisplay GetEmpty(int blueprintId)
public ContentItemDisplay GetEmpty(int blueprintId, int parentId)
{
var blueprint = Services.ContentService.GetBlueprintById(blueprintId);
if (blueprint == null)
@@ -309,6 +314,7 @@ namespace Umbraco.Web.Editors
blueprint.Id = 0;
blueprint.Name = string.Empty;
blueprint.ParentId = parentId;
var mapped = Mapper.Map<ContentItemDisplay>(blueprint);
@@ -83,10 +83,10 @@ namespace Umbraco.Web.Editors
contentIdToCheck = contentToCheck.Id;
break;
case ContentSaveAction.SaveNew:
//Save new requires both ActionNew AND ActionUpdate
//Save new requires ActionNew
permissionToCheck.Add(ActionNew.Instance.Letter);
permissionToCheck.Add(ActionNew.Instance.Letter);
permissionToCheck.Add(ActionUpdate.Instance.Letter);
if (contentItem.ParentId != Constants.System.Root)
{
contentToCheck = ContentService.GetById(contentItem.ParentId);
@@ -98,7 +98,7 @@ namespace Umbraco.Web.Editors
}
break;
case ContentSaveAction.SendPublishNew:
//Send new requires both ActionToPublish AND ActionUpdate
//Send new requires both ActionToPublish AND ActionNew
permissionToCheck.Add(ActionNew.Instance.Letter);
permissionToCheck.Add(ActionToPublish.Instance.Letter);
@@ -114,6 +114,7 @@ namespace Umbraco.Web.Editors
break;
case ContentSaveAction.PublishNew:
//Publish new requires both ActionNew AND ActionPublish
//TODO: Shoudn't publish also require ActionUpdate since it will definitely perform an update to publish but maybe that's just implied
permissionToCheck.Add(ActionNew.Instance.Letter);
permissionToCheck.Add(ActionPublish.Instance.Letter);
@@ -159,6 +159,15 @@ namespace Umbraco.Web.Editors
: Request.CreateNotificationValidationErrorResponse(result.Exception.Message);
}
public HttpResponseMessage PostRenameContainer(int id, string name)
{
var result = Services.ContentTypeService.RenameContainer(id, name, Security.CurrentUser.Id);
return result
? Request.CreateResponse(HttpStatusCode.OK, result.Result) //return the id
: Request.CreateNotificationValidationErrorResponse(result.Exception.Message);
}
public DocumentTypeDisplay PostSave(DocumentTypeSave contentTypeSave)
{
var savedCt = PerformPostSave<DocumentTypeDisplay, DocumentTypeSave, PropertyTypeBasic>(
@@ -279,8 +288,8 @@ namespace Umbraco.Web.Editors
{
basic.Name = localizedTextService.UmbracoDictionaryTranslate(basic.Name);
basic.Description = localizedTextService.UmbracoDictionaryTranslate(basic.Description);
}
}
//map the blueprints
var blueprints = Services.ContentService.GetBlueprintsForContentTypes(types.Select(x => x.Id).ToArray()).ToArray();
foreach (var basic in basics)
@@ -290,7 +299,7 @@ namespace Umbraco.Web.Editors
{
basic.Blueprints[blueprint.Id] = blueprint.Name;
}
}
}
return basics;
}
@@ -80,7 +80,7 @@ namespace Umbraco.Web.Editors
public async Task<ModelWithNotifications<string>> PostChangePassword(ChangingPasswordModel data)
{
var passwordChanger = new PasswordChanger(Logger, Services.UserService);
var passwordChangeResult = await passwordChanger.ChangePasswordWithIdentityAsync(Security.CurrentUser, data, ModelState, UserManager);
var passwordChangeResult = await passwordChanger.ChangePasswordWithIdentityAsync(Security.CurrentUser, Security.CurrentUser, data, UserManager);
if (passwordChangeResult.Success)
{
@@ -90,6 +90,11 @@ namespace Umbraco.Web.Editors
return result;
}
foreach (var memberName in passwordChangeResult.Result.ChangeError.MemberNames)
{
ModelState.AddModelError(memberName, passwordChangeResult.Result.ChangeError.ErrorMessage);
}
throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState));
}
+2 -48
View File
@@ -123,54 +123,8 @@ namespace Umbraco.Web.Editors
[ValidateAngularAntiForgeryToken]
public IEnumerable<Tab<DashboardControl>> GetDashboard(string section)
{
var tabs = new List<Tab<DashboardControl>>();
var i = 1;
// The dashboard config can contain more than one area inserted by a package.
foreach( var dashboardSection in UmbracoConfig.For.DashboardSettings().Sections.Where(x => x.Areas.Contains(section)))
{
//we need to validate access to this section
if (DashboardSecurity.AuthorizeAccess(dashboardSection, Security.CurrentUser, Services.SectionService) == false)
continue;
//User is authorized
foreach (var tab in dashboardSection.Tabs)
{
//we need to validate access to this tab
if (DashboardSecurity.AuthorizeAccess(tab, Security.CurrentUser, Services.SectionService) == false)
continue;
var dashboardControls = new List<DashboardControl>();
foreach (var control in tab.Controls)
{
if (DashboardSecurity.AuthorizeAccess(control, Security.CurrentUser, Services.SectionService) == false)
continue;
var dashboardControl = new DashboardControl();
var controlPath = control.ControlPath.Trim();
dashboardControl.Path = IOHelper.FindFile(controlPath);
if (controlPath.ToLowerInvariant().EndsWith(".ascx".ToLowerInvariant()))
dashboardControl.ServerSide = true;
dashboardControls.Add(dashboardControl);
}
tabs.Add(new Tab<DashboardControl>
{
Id = i,
Alias = tab.Caption.ToSafeAlias(),
IsActive = i == 1,
Label = tab.Caption,
Properties = dashboardControls
});
i++;
}
}
//In case there are no tabs or a user doesn't have access the empty tabs list is returned
return tabs;
var dashboardHelper = new DashboardHelper(Services.SectionService);
return dashboardHelper.GetDashboard(section, Security.CurrentUser);
}
}
}
@@ -0,0 +1,96 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Umbraco.Core;
using Umbraco.Core.Configuration;
using Umbraco.Core.IO;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Services;
using Umbraco.Web.Models.ContentEditing;
namespace Umbraco.Web.Editors
{
internal class DashboardHelper
{
private readonly ISectionService _sectionService;
public DashboardHelper(ISectionService sectionService)
{
if (sectionService == null) throw new ArgumentNullException("sectionService");
_sectionService = sectionService;
}
/// <summary>
/// Returns the dashboard models per section for the current user and it's access
/// </summary>
/// <param name="currentUser"></param>
/// <returns></returns>
public IDictionary<string, IEnumerable<Tab<DashboardControl>>> GetDashboards(IUser currentUser)
{
var result = new Dictionary<string, IEnumerable<Tab<DashboardControl>>>();
foreach (var section in _sectionService.GetSections())
{
result[section.Alias] = GetDashboard(section.Alias, currentUser);
}
return result;
}
/// <summary>
/// Returns the dashboard model for the given section based on the current user and it's access
/// </summary>
/// <param name="section"></param>
/// <param name="currentUser"></param>
/// <returns></returns>
public IEnumerable<Tab<DashboardControl>> GetDashboard(string section, IUser currentUser)
{
var tabs = new List<Tab<DashboardControl>>();
var i = 1;
// The dashboard config can contain more than one area inserted by a package.
foreach (var dashboardSection in UmbracoConfig.For.DashboardSettings().Sections.Where(x => x.Areas.Contains(section)))
{
//we need to validate access to this section
if (DashboardSecurity.AuthorizeAccess(dashboardSection, currentUser, _sectionService) == false)
continue;
//User is authorized
foreach (var tab in dashboardSection.Tabs)
{
//we need to validate access to this tab
if (DashboardSecurity.AuthorizeAccess(tab, currentUser, _sectionService) == false)
continue;
var dashboardControls = new List<DashboardControl>();
foreach (var control in tab.Controls)
{
if (DashboardSecurity.AuthorizeAccess(control, currentUser, _sectionService) == false)
continue;
var dashboardControl = new DashboardControl();
var controlPath = control.ControlPath.Trim();
dashboardControl.Path = IOHelper.FindFile(controlPath);
if (controlPath.ToLowerInvariant().EndsWith(".ascx".ToLowerInvariant()))
dashboardControl.ServerSide = true;
dashboardControls.Add(dashboardControl);
}
tabs.Add(new Tab<DashboardControl>
{
Id = i,
Alias = tab.Caption.ToSafeAlias(),
IsActive = i == 1,
Label = tab.Caption,
Properties = dashboardControls
});
i++;
}
}
//In case there are no tabs or a user doesn't have access the empty tabs list is returned
return tabs;
}
}
}
+12 -3
View File
@@ -20,7 +20,7 @@ using Umbraco.Web.Composing;
namespace Umbraco.Web.Editors
{
/// <summary>
/// The API controller used for editing data types
/// </summary>
@@ -113,7 +113,7 @@ namespace Umbraco.Web.Editors
//if it doesnt exist yet, we will create it.
if (dt == null)
{
dt = new DataTypeDefinition( Constants.PropertyEditors.ListViewAlias );
dt = new DataTypeDefinition(Constants.PropertyEditors.ListViewAlias);
dt.Name = Constants.Conventions.DataTypes.ListViewPrefix + contentTypeAlias;
Services.DataTypeService.Save(dt);
}
@@ -265,6 +265,15 @@ namespace Umbraco.Web.Editors
}
}
public HttpResponseMessage PostRenameContainer(int id, string name)
{
var result = Services.DataTypeService.RenameContainer(id, name, Security.CurrentUser.Id);
return result
? Request.CreateResponse(HttpStatusCode.OK, result.Result)
: Request.CreateNotificationValidationErrorResponse(result.Exception.Message);
}
#region ReadOnly actions to return basic data - allow access for: content ,media, members, settings, developer
/// <summary>
/// Gets the content json for all data types
@@ -305,7 +314,7 @@ namespace Umbraco.Web.Editors
foreach (var dataType in dataTypes)
{
var propertyEditor = propertyEditors.SingleOrDefault(x => x.Alias == dataType.Alias);
if(propertyEditor != null)
if (propertyEditor != null)
dataType.HasPrevalues = propertyEditor.PreValueEditor.Fields.Any(); ;
}
@@ -153,6 +153,16 @@ namespace Umbraco.Web.Editors
: Request.CreateNotificationValidationErrorResponse(result.Exception.Message);
}
public HttpResponseMessage PostRenameContainer(int id, string name)
{
var result = Services.MediaTypeService.RenameContainer(id, name, Security.CurrentUser.Id);
return result
? Request.CreateResponse(HttpStatusCode.OK, result.Result) //return the id
: Request.CreateNotificationValidationErrorResponse(result.Exception.Message);
}
public MediaTypeDisplay PostSave(MediaTypeSave contentTypeSave)
{
var savedCt = PerformPostSave<MediaTypeDisplay, MediaTypeSave, PropertyTypeBasic>(
+38 -13
View File
@@ -5,6 +5,7 @@ using System.Web.Http.ModelBinding;
using System.Web.Security;
using Umbraco.Core;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Identity;
using Umbraco.Core.Security;
using Umbraco.Core.Services;
@@ -24,10 +25,18 @@ namespace Umbraco.Web.Editors
_userService = userService;
}
/// <summary>
/// Changes the password for a user based on the many different rules and config options
/// </summary>
/// <param name="currentUser">The user performing the password save action</param>
/// <param name="savingUser">The user who's password is being changed</param>
/// <param name="passwordModel"></param>
/// <param name="userMgr"></param>
/// <returns></returns>
public async Task<Attempt<PasswordChangedModel>> ChangePasswordWithIdentityAsync(
IUser currentUser,
IUser savingUser,
ChangingPasswordModel passwordModel,
ModelStateDictionary modelState,
BackOfficeUserManager<BackOfficeIdentityUser> userMgr)
{
if (passwordModel == null) throw new ArgumentNullException(nameof(passwordModel));
@@ -43,7 +52,7 @@ namespace Umbraco.Web.Editors
//if this isn't using an IUserAwarePasswordHasher, then fallback to the old way
if (membershipPasswordHasher.MembershipProvider.RequiresQuestionAndAnswer)
throw new NotSupportedException("Currently the user editor does not support providers that have RequiresQuestionAndAnswer specified");
return ChangePasswordWithMembershipProvider(currentUser.Username, passwordModel, membershipPasswordHasher.MembershipProvider);
return ChangePasswordWithMembershipProvider(savingUser.Username, passwordModel, membershipPasswordHasher.MembershipProvider);
}
//if we are here, then a IUserAwarePasswordHasher is available, however we cannot proceed in that case if for some odd reason
@@ -54,22 +63,38 @@ namespace Umbraco.Web.Editors
throw new InvalidOperationException("The membership provider cannot have a password format of " + membershipPasswordHasher.MembershipProvider.PasswordFormat + " and be configured with secured hashed passwords");
}
//Are we resetting the password??
//Are we resetting the password?? In ASP.NET Identity APIs, this flag indicates that an admin user is changing another user's password
//without knowing the original password.
if (passwordModel.Reset.HasValue && passwordModel.Reset.Value)
{
//if it's the current user, the current user cannot reset their own password
if (currentUser.Username == savingUser.Username)
{
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password reset is not allowed", new[] { "resetPassword" }) });
}
//if the current user has access to reset/manually change the password
if (currentUser.HasSectionAccess(Umbraco.Core.Constants.Applications.Users) == false)
{
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("The current user is not authorized", new[] { "resetPassword" }) });
}
//ok, we should be able to reset it
var resetToken = await userMgr.GeneratePasswordResetTokenAsync(currentUser.Id);
var newPass = userMgr.GeneratePassword();
var resetResult = await userMgr.ResetPasswordAsync(currentUser.Id, resetToken, newPass);
var resetToken = await userMgr.GeneratePasswordResetTokenAsync(savingUser.Id);
var newPass = passwordModel.NewPassword.IsNullOrWhiteSpace()
? userMgr.GeneratePassword()
: passwordModel.NewPassword;
var resetResult = await userMgr.ResetPasswordAsync(savingUser.Id, resetToken, newPass);
if (resetResult.Succeeded == false)
{
var errors = string.Join(". ", resetResult.Errors);
_logger.Warn<PasswordChanger>($"Could not reset member password {errors}");
_logger.Warn<PasswordChanger>($"Could not reset user password {errors}");
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not reset password, errors: " + errors, new[] { "resetPassword" }) });
}
return Attempt.Succeed(new PasswordChangedModel { ResetPassword = newPass });
return Attempt.Succeed(new PasswordChangedModel());
}
//we're not resetting it so we need to try to change it.
@@ -90,12 +115,12 @@ namespace Umbraco.Web.Editors
if (passwordModel.OldPassword.IsNullOrWhiteSpace() == false)
{
//if an old password is suplied try to change it
var changeResult = await userMgr.ChangePasswordAsync(currentUser.Id, passwordModel.OldPassword, passwordModel.NewPassword);
var changeResult = await userMgr.ChangePasswordAsync(savingUser.Id, passwordModel.OldPassword, passwordModel.NewPassword);
if (changeResult.Succeeded == false)
{
var errors = string.Join(". ", changeResult.Errors);
_logger.Warn<PasswordChanger>($"Could not change member password {errors}");
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, errors: " + errors, new[] { "value" }) });
_logger.Warn<PasswordChanger>($"Could not change user password {errors}");
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, errors: " + errors, new[] { "oldPassword" }) });
}
return Attempt.Succeed(new PasswordChangedModel());
}
@@ -107,7 +132,7 @@ namespace Umbraco.Web.Editors
/// <summary>
/// Changes password for a member/user given the membership provider and the password change model
/// </summary>
/// <param name="username"></param>
/// <param name="username">The username of the user having their password changed</param>
/// <param name="passwordModel"></param>
/// <param name="membershipProvider"></param>
/// <returns></returns>
+49 -2
View File
@@ -3,6 +3,9 @@ using AutoMapper;
using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Mvc;
using System.Linq;
using Umbraco.Core.Models;
using Umbraco.Web.Trees;
using Section = Umbraco.Web.Models.ContentEditing.Section;
namespace Umbraco.Web.Editors
{
@@ -14,14 +17,58 @@ namespace Umbraco.Web.Editors
{
public IEnumerable<Section> GetSections()
{
var sections = Services.SectionService.GetAllowedSections(Security.GetUserId());
return sections.Select(Mapper.Map<Core.Models.Section, Section>);
var sectionModels = sections.Select(Mapper.Map<Core.Models.Section, Section>).ToArray();
//Check if there are empty dashboards or dashboards that will end up empty based on the current user's access
//and add the meta data about them
var dashboardHelper = new DashboardHelper(Services.SectionService);
//this is a bit nasty since we'll be proxying via the app tree controller but we sort of have to do that
//since tree's by nature are controllers and require request contextual data.
var appTreeController = new ApplicationTreeController
{
ControllerContext = ControllerContext
};
var dashboards = dashboardHelper.GetDashboards(Security.CurrentUser);
//now we can add metadata for each section so that the UI knows if there's actually anything at all to render for
//a dashboard for a given section, then the UI can deal with it accordingly (i.e. redirect to the first tree)
foreach (var section in sectionModels)
{
var hasDashboards = false;
IEnumerable<Tab<DashboardControl>> dashboardsForSection;
if (dashboards.TryGetValue(section.Alias, out dashboardsForSection))
{
if (dashboardsForSection.Any())
hasDashboards = true;
}
if (hasDashboards == false)
{
//get the first tree in the section and get it's root node route path
var sectionTrees = appTreeController.GetApplicationTrees(section.Alias, null, null).Result;
section.RoutePath = sectionTrees.IsContainer == false
? sectionTrees.RoutePath
: sectionTrees.Children[0].RoutePath;
}
}
return sectionModels;
}
/// <summary>
/// Returns all the sections that the user has access to
/// </summary>
/// <returns></returns>
public IEnumerable<Section> GetAllSections()
{
var sections = Services.SectionService.GetSections();
return sections.Select(Mapper.Map<Core.Models.Section, Section>);
var mapped = sections.Select(Mapper.Map<Core.Models.Section, Section>);
if (Security.CurrentUser.IsAdmin())
return mapped;
return mapped.Where(x => Security.CurrentUser.AllowedSections.Contains(x.Alias)).ToArray();
}
}
@@ -0,0 +1,155 @@
using System.Collections.Generic;
using System.Linq;
using Umbraco.Core;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Services;
namespace Umbraco.Web.Editors
{
internal class UserEditorAuthorizationHelper
{
private readonly IContentService _contentService;
private readonly IMediaService _mediaService;
private readonly IUserService _userService;
private readonly IEntityService _entityService;
public UserEditorAuthorizationHelper(IContentService contentService, IMediaService mediaService, IUserService userService, IEntityService entityService)
{
_contentService = contentService;
_mediaService = mediaService;
_userService = userService;
_entityService = entityService;
}
/// <summary>
/// Checks if the current user has access to save the user data
/// </summary>
/// <param name="currentUser">The current user trying to save user data</param>
/// <param name="savingUser">The user instance being saved (can be null if it's a new user)</param>
/// <param name="startContentIds">The start content ids of the user being saved (can be null or empty)</param>
/// <param name="startMediaIds">The start media ids of the user being saved (can be null or empty)</param>
/// <param name="userGroupAliases">The user aliases of the user being saved (can be null or empty)</param>
/// <returns></returns>
public Attempt<string> IsAuthorized(IUser currentUser,
IUser savingUser,
IEnumerable<int> startContentIds, IEnumerable<int> startMediaIds,
IEnumerable<string> userGroupAliases)
{
var currentIsAdmin = currentUser.IsAdmin();
// a) A non-admin cannot save an admin
if (savingUser != null)
{
if (savingUser.IsAdmin() && currentIsAdmin == false)
return Attempt.Fail("The current user is not an administrator so cannot save another administrator");
}
// b) If a start node is changing, a user cannot set a start node on another user that they don't have access to, this even goes for admins
//only validate any start nodes that have changed.
//a user can remove any start nodes and add start nodes that they have access to
//but they cannot add a start node that they do not have access to
var changedStartContentIds = savingUser == null
? startContentIds
: startContentIds == null
? null
: startContentIds.Except(savingUser.StartContentIds).ToArray();
var changedStartMediaIds = savingUser == null
? startMediaIds
: startMediaIds == null
? null
: startMediaIds.Except(savingUser.StartMediaIds).ToArray();
var pathResult = AuthorizePath(currentUser, changedStartContentIds, changedStartMediaIds);
if (pathResult == false)
return pathResult;
// c) an admin can manage any group or section access
if (currentIsAdmin)
return Attempt<string>.Succeed();
if (userGroupAliases != null)
{
var savingGroupAliases = userGroupAliases.ToArray();
//only validate any groups that have changed.
//a non-admin user can remove groups and add groups that they have access to
//but they cannot add a group that they do not have access to or that grants them
//path or section access that they don't have access to.
var newGroups = savingUser == null
? savingGroupAliases
: savingGroupAliases.Except(savingUser.Groups.Select(x => x.Alias)).ToArray();
var userGroupsChanged = savingUser != null && newGroups.Length > 0;
if (userGroupsChanged)
{
// d) A user cannot assign a group to another user that they do not belong to
var currentUserGroups = currentUser.Groups.Select(x => x.Alias).ToArray();
foreach (var group in newGroups)
{
if (currentUserGroups.Contains(group) == false)
{
return Attempt.Fail("Cannot assign the group " + group + ", the current user is not a member");
}
}
}
}
return Attempt<string>.Succeed();
}
private Attempt<string> AuthorizePath(IUser currentUser, IEnumerable<int> startContentIds, IEnumerable<int> startMediaIds)
{
if (startContentIds != null)
{
foreach (var contentId in startContentIds)
{
if (contentId == Constants.System.Root)
{
var hasAccess = UserExtensions.HasPathAccess("-1", currentUser.CalculateContentStartNodeIds(_entityService), Constants.System.RecycleBinContent);
if (hasAccess == false)
return Attempt.Fail("The current user does not have access to the content root");
}
else
{
var content = _contentService.GetById(contentId);
if (content == null) continue;
var hasAccess = currentUser.HasPathAccess(content, _entityService);
if (hasAccess == false)
return Attempt.Fail("The current user does not have access to the content path " + content.Path);
}
}
}
if (startMediaIds != null)
{
foreach (var mediaId in startMediaIds)
{
if (mediaId == Constants.System.Root)
{
var hasAccess = UserExtensions.HasPathAccess("-1", currentUser.CalculateMediaStartNodeIds(_entityService), Constants.System.RecycleBinMedia);
if (hasAccess == false)
return Attempt.Fail("The current user does not have access to the media root");
}
else
{
var media = _mediaService.GetById(mediaId);
if (media == null) continue;
var hasAccess = currentUser.HasPathAccess(media, _entityService);
if (hasAccess == false)
return Attempt.Fail("The current user does not have access to the media path " + media.Path);
}
}
}
return Attempt<string>.Succeed();
}
}
}
@@ -0,0 +1,62 @@
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using System.Web.Http.Controllers;
using Umbraco.Core;
using Umbraco.Core.Composing;
namespace Umbraco.Web.Editors
{
/// <summary>
/// Authorizes that the current user has access to the user group Id in the request
/// </summary>
internal class UserGroupAuthorizationAttribute : AuthorizeAttribute
{
private readonly string _paramName;
private readonly UmbracoContext _umbracoContext;
/// <summary>
/// THIS SHOULD BE ONLY USED FOR UNIT TESTS
/// </summary>
/// <param name="paramName"></param>
/// <param name="umbracoContext"></param>
public UserGroupAuthorizationAttribute(string paramName, UmbracoContext umbracoContext)
{
if (umbracoContext == null) throw new ArgumentNullException("umbracoContext");
_paramName = paramName;
_umbracoContext = umbracoContext;
}
public UserGroupAuthorizationAttribute(string paramName)
{
_paramName = paramName;
}
private UmbracoContext GetUmbracoContext()
{
return _umbracoContext ?? UmbracoContext.Current;
}
protected override bool IsAuthorized(HttpActionContext actionContext)
{
var umbCtx = GetUmbracoContext();
var currentUser = umbCtx.Security.CurrentUser;
var queryString = actionContext.Request.GetQueryNameValuePairs();
var ids = queryString.Where(x => x.Key == _paramName).ToArray();
if (ids.Length == 0)
return base.IsAuthorized(actionContext);
var intIds = ids.Select(x => x.Value.TryConvertTo<int>()).Where(x => x.Success).Select(x => x.Result).ToArray();
var authHelper = new UserGroupEditorAuthorizationHelper(
Current.Services.UserService,
Current.Services.ContentService,
Current.Services.MediaService,
Current.Services.EntityService);
return authHelper.AuthorizeGroupAccess(currentUser, intIds);
}
}
}
@@ -0,0 +1,122 @@
using System.Collections.Generic;
using System.Linq;
using Umbraco.Core;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Services;
namespace Umbraco.Web.Editors
{
internal class UserGroupEditorAuthorizationHelper
{
private readonly IUserService _userService;
private readonly IContentService _contentService;
private readonly IMediaService _mediaService;
private readonly IEntityService _entityService;
public UserGroupEditorAuthorizationHelper(IUserService userService, IContentService contentService, IMediaService mediaService, IEntityService entityService)
{
_userService = userService;
_contentService = contentService;
_mediaService = mediaService;
_entityService = entityService;
}
/// <summary>
/// Authorize that the current user belongs to these groups
/// </summary>
/// <param name="currentUser"></param>
/// <param name="groupIds"></param>
/// <returns></returns>
public Attempt<string> AuthorizeGroupAccess(IUser currentUser, params int[] groupIds)
{
if (currentUser.IsAdmin())
return Attempt<string>.Succeed();
var groups = _userService.GetAllUserGroups(groupIds.ToArray());
var groupAliases = groups.Select(x => x.Alias).ToArray();
var userGroups = currentUser.Groups.Select(x => x.Alias).ToArray();
var missingAccess = groupAliases.Except(userGroups).ToArray();
return missingAccess.Length == 0
? Attempt<string>.Succeed()
: Attempt.Fail("User is not a member of " + string.Join(", ", missingAccess));
}
/// <summary>
/// Authorize that the current user belongs to these groups
/// </summary>
/// <param name="currentUser"></param>
/// <param name="groupAliases"></param>
/// <returns></returns>
public Attempt<string> AuthorizeGroupAccess(IUser currentUser, params string[] groupAliases)
{
if (currentUser.IsAdmin())
return Attempt<string>.Succeed();
var userGroups = currentUser.Groups.Select(x => x.Alias).ToArray();
var missingAccess = groupAliases.Except(userGroups).ToArray();
return missingAccess.Length == 0
? Attempt<string>.Succeed()
: Attempt.Fail("User is not a member of " + string.Join(", ", missingAccess));
}
/// <summary>
/// Authorize that the user is not adding a section to the group that they don't have access to
/// </summary>
/// <param name="currentUser"></param>
/// <param name="currentAllowedSections"></param>
/// <param name="proposedAllowedSections"></param>
/// <returns></returns>
public Attempt<string> AuthorizeSectionChanges(IUser currentUser,
IEnumerable<string> currentAllowedSections,
IEnumerable<string> proposedAllowedSections)
{
if (currentUser.IsAdmin())
return Attempt<string>.Succeed();
var sectionsAdded = currentAllowedSections.Except(proposedAllowedSections).ToArray();
var sectionAccessMissing = sectionsAdded.Except(currentUser.AllowedSections).ToArray();
return sectionAccessMissing.Length > 0
? Attempt.Fail("Current user doesn't have access to add these sections " + string.Join(", ", sectionAccessMissing))
: Attempt<string>.Succeed();
}
/// <summary>
/// Authorize that the user is not changing to a start node that they don't have access to (including admins)
/// </summary>
/// <param name="currentUser"></param>
/// <param name="currentContentStartId"></param>
/// <param name="proposedContentStartId"></param>
/// <param name="currentMediaStartId"></param>
/// <param name="proposedMediaStartId"></param>
/// <returns></returns>
public Attempt<string> AuthorizeStartNodeChanges(IUser currentUser,
int? currentContentStartId,
int? proposedContentStartId,
int? currentMediaStartId,
int? proposedMediaStartId)
{
if (currentContentStartId != proposedContentStartId && proposedContentStartId.HasValue)
{
var content = _contentService.GetById(proposedContentStartId.Value);
if (content != null)
{
if (currentUser.HasPathAccess(content, _entityService) == false)
return Attempt.Fail("Current user doesn't have access to the content path " + content.Path);
}
}
if (currentMediaStartId != proposedMediaStartId && proposedMediaStartId.HasValue)
{
var media = _mediaService.GetById(proposedMediaStartId.Value);
if (media != null)
{
if (currentUser.HasPathAccess(media, _entityService) == false)
return Attempt.Fail("Current user doesn't have access to the media path " + media.Path);
}
}
return Attempt<string>.Succeed();
}
}
}
@@ -4,7 +4,9 @@ using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using System.Web.Http.Filters;
using AutoMapper;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Services;
using Umbraco.Web.Models.ContentEditing;
@@ -25,9 +27,32 @@ namespace Umbraco.Web.Editors
{
if (userGroupSave == null) throw new ArgumentNullException(nameof(userGroupSave));
//authorize that the user has access to save this user group
var authHelper = new UserGroupEditorAuthorizationHelper(
Services.UserService, Services.ContentService, Services.MediaService, Services.EntityService);
var isAuthorized = authHelper.AuthorizeGroupAccess(Security.CurrentUser, userGroupSave.Alias);
if (isAuthorized == false)
throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.Unauthorized, isAuthorized.Result));
//if sections were added we need to check that the current user has access to that section
isAuthorized = authHelper.AuthorizeSectionChanges(Security.CurrentUser,
userGroupSave.PersistedUserGroup.AllowedSections,
userGroupSave.Sections);
if (isAuthorized == false)
throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.Unauthorized, isAuthorized.Result));
//if start nodes were changed we need to check that the current user has access to them
isAuthorized = authHelper.AuthorizeStartNodeChanges(Security.CurrentUser,
userGroupSave.PersistedUserGroup.StartContentId,
userGroupSave.StartContentId,
userGroupSave.PersistedUserGroup.StartMediaId,
userGroupSave.StartMediaId);
if (isAuthorized == false)
throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.Unauthorized, isAuthorized.Result));
//save the group
Services.UserService.Save(userGroupSave.PersistedUserGroup, userGroupSave.Users.ToArray());
//deal with permissions
//remove ones that have been removed
@@ -67,15 +92,31 @@ namespace Umbraco.Web.Editors
/// Returns all user groups
/// </summary>
/// <returns></returns>
public IEnumerable<UserGroupBasic> GetUserGroups()
public IEnumerable<UserGroupBasic> GetUserGroups(bool onlyCurrentUserGroups = true)
{
return Mapper.Map<IEnumerable<IUserGroup>, IEnumerable<UserGroupBasic>>(Services.UserService.GetAllUserGroups());
var allGroups = Mapper.Map<IEnumerable<IUserGroup>, IEnumerable<UserGroupBasic>>(Services.UserService.GetAllUserGroups())
.ToList();
var isAdmin = Security.CurrentUser.IsAdmin();
if (isAdmin) return allGroups;
if (onlyCurrentUserGroups == false)
{
//this user is not an admin so in that case we need to exlude all admin users
allGroups.RemoveAt(allGroups.IndexOf(allGroups.Find(basic => basic.Alias == Constants.Security.AdminGroupAlias)));
return allGroups;
}
//we cannot return user groups that this user does not have access to
var currentUserGroups = Security.CurrentUser.Groups.Select(x => x.Alias).ToArray();
return allGroups.Where(x => currentUserGroups.Contains(x.Alias)).ToArray();
}
/// <summary>
/// Return a user group
/// </summary>
/// <returns></returns>
[UserGroupAuthorization("id")]
public UserGroupDisplay GetUserGroup(int id)
{
var found = Services.UserService.GetUserGroupById(id);
@@ -89,9 +130,13 @@ namespace Umbraco.Web.Editors
[HttpPost]
[HttpDelete]
[UserGroupAuthorization("userGroupIds")]
public HttpResponseMessage PostDeleteUserGroups([FromUri] int[] userGroupIds)
{
var userGroups = Services.UserService.GetAllUserGroups(userGroupIds).ToArray();
var userGroups = Services.UserService.GetAllUserGroups(userGroupIds)
//never delete the admin group or translators group
.Where(x => x.Alias != Constants.Security.AdminGroupAlias && x.Alias != Constants.Security.TranslatorGroupAlias)
.ToArray();
foreach (var userGroup in userGroups)
{
Services.UserService.DeleteUserGroup(userGroup);
+161 -38
View File
@@ -8,6 +8,7 @@ using System.Runtime.Serialization;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Mvc;
using AutoMapper;
using Microsoft.AspNet.Identity;
@@ -20,11 +21,14 @@ using Umbraco.Core.Models;
using Umbraco.Core.Models.Identity;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
using Umbraco.Core.Persistence.Querying;
using Umbraco.Core.Security;
using Umbraco.Core.Services;
using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Mvc;
using Umbraco.Web.WebApi;
using Umbraco.Web.WebApi.Filters;
using ActionFilterAttribute = System.Web.Http.Filters.ActionFilterAttribute;
using Constants = Umbraco.Core.Constants;
using IUser = Umbraco.Core.Models.Membership.IUser;
using Task = System.Threading.Tasks.Task;
@@ -124,7 +128,7 @@ namespace Umbraco.Web.Editors
var filePath = found.Avatar;
//if the filePath is already null it will mean that the user doesn't have a custom avatar and their gravatar is currently
//being used (if they have one). This means they want to remove their gravatar too which we can do by setting a special value
//being used (if they have one). This means they want to remove their gravatar too which we can do by setting a special value
//for the avatar.
if (filePath.IsNullOrWhiteSpace() == false)
{
@@ -159,11 +163,10 @@ namespace Umbraco.Web.Editors
{
throw new HttpResponseException(HttpStatusCode.NotFound);
}
return Mapper.Map<IUser, UserDisplay>(user);
var result = Mapper.Map<IUser, UserDisplay>(user);
return result;
}
/// <summary>
/// Returns a paged users collection
/// </summary>
@@ -184,10 +187,37 @@ namespace Umbraco.Web.Editors
[FromUri]UserState[] userStates = null,
string filter = "")
{
//following the same principle we had in previous versions, we would only show admins to admins, see
// https://github.com/umbraco/Umbraco-CMS/blob/dev-v7/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/loadUsers.cs#L91
// so to do that here, we'll need to check if this current user is an admin and if not we should exclude all user who are
// also admins
var excludeUserGroups = new string[0];
var isAdmin = Security.CurrentUser.IsAdmin();
if (isAdmin == false)
{
//this user is not an admin so in that case we need to exlude all admin users
excludeUserGroups = new[] {Constants.Security.AdminGroupAlias};
}
var filterQuery = Current.DatabaseContext.Query<IUser>();
//if the current user is not the administrator, then don't include this in the results.
var isAdminUser = Security.CurrentUser.Id == 0;
if (isAdminUser == false)
{
filterQuery.Where(x => x.Id != 0);
}
if (filter.IsNullOrWhiteSpace() == false)
{
filterQuery.Where(x => x.Name.Contains(filter) || x.Username.Contains(filter));
}
long pageIndex = pageNumber - 1;
long total;
var result = Services.UserService.GetAll(pageIndex, pageSize, out total, orderBy, orderDirection, userStates, userGroups, filter);
var result = Services.UserService.GetAll(pageIndex, pageSize, out total, orderBy, orderDirection, userStates, userGroups, excludeUserGroups, filterQuery);
var paged = new PagedUserResult(total, pageNumber, pageSize)
{
Items = Mapper.Map<IEnumerable<UserBasic>>(result),
@@ -211,16 +241,29 @@ namespace Umbraco.Web.Editors
throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState));
}
var existing = Services.UserService.GetByEmail(userSave.Email);
if (existing != null)
if (UmbracoConfig.For.UmbracoSettings().Security.UsernameIsEmail)
{
ModelState.AddModelError("Email", "A user with the email already exists");
throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState));
//ensure they are the same if we're using it
userSave.Username = userSave.Email;
}
else
{
//first validate the username if were showing it
CheckUniqueUsername(userSave.Username, null);
}
CheckUniqueEmail(userSave.Email, null);
//Perform authorization here to see if the current user can actually save this user with the info being requested
var authHelper = new UserEditorAuthorizationHelper(Services.ContentService, Services.MediaService, Services.UserService, Services.EntityService);
var canSaveUser = authHelper.IsAuthorized(Security.CurrentUser, null, null, null, userSave.UserGroups);
if (canSaveUser == false)
{
throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.Unauthorized, canSaveUser.Result));
}
//we want to create the user with the UserManager, this ensures the 'empty' (special) password
//format is applied without us having to duplicate that logic
var identityUser = BackOfficeIdentityUser.CreateNew(userSave.Email, userSave.Email, GlobalSettings.DefaultUILanguage);
var identityUser = BackOfficeIdentityUser.CreateNew(userSave.Username, userSave.Email, GlobalSettings.DefaultUILanguage);
identityUser.Name = userSave.Name;
var created = await UserManager.CreateAsync(identityUser);
@@ -266,7 +309,7 @@ namespace Umbraco.Web.Editors
/// <summary>
/// Invites a user
/// </summary>
/// <param name="userSave"></param>
/// <param name="userSave"></param>
/// <returns></returns>
/// <remarks>
/// This will email the user an invite and generate a token that will be validated in the email
@@ -283,25 +326,38 @@ namespace Umbraco.Web.Editors
throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState));
}
var hasSmtp = GlobalSettings.HasSmtpServerConfigured(RequestContext.VirtualPathRoot);
if (hasSmtp == false)
if (EmailSender.CanSendRequiredEmail == false)
{
throw new HttpResponseException(
Request.CreateNotificationValidationErrorResponse("No Email server is configured"));
}
var user = Services.UserService.GetByEmail(userSave.Email);
if (user != null && (user.LastLoginDate != default(DateTime) || user.EmailConfirmedDate.HasValue))
IUser user;
if (UmbracoConfig.For.UmbracoSettings().Security.UsernameIsEmail)
{
ModelState.AddModelError("Email", "A user with the email already exists");
throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState));
//ensure it's the same
userSave.Username = userSave.Email;
}
else
{
//first validate the username if we're showing it
user = CheckUniqueUsername(userSave.Username, u => u.LastLoginDate != default(DateTime) || u.EmailConfirmedDate.HasValue);
}
user = CheckUniqueEmail(userSave.Email, u => u.LastLoginDate != default(DateTime) || u.EmailConfirmedDate.HasValue);
//Perform authorization here to see if the current user can actually save this user with the info being requested
var authHelper = new UserEditorAuthorizationHelper(Services.ContentService, Services.MediaService, Services.UserService, Services.EntityService);
var canSaveUser = authHelper.IsAuthorized(Security.CurrentUser, user, null, null, userSave.UserGroups);
if (canSaveUser == false)
{
throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.Unauthorized, canSaveUser.Result));
}
if (user == null)
{
//we want to create the user with the UserManager, this ensures the 'empty' (special) password
//format is applied without us having to duplicate that logic
var identityUser = BackOfficeIdentityUser.CreateNew(userSave.Email, userSave.Email, GlobalSettings.DefaultUILanguage);
var identityUser = BackOfficeIdentityUser.CreateNew(userSave.Username, userSave.Email, GlobalSettings.DefaultUILanguage);
identityUser.Name = userSave.Name;
var created = await UserManager.CreateAsync(identityUser);
@@ -332,7 +388,29 @@ namespace Umbraco.Web.Editors
return display;
}
private IUser CheckUniqueEmail(string email, Func<IUser, bool> extraCheck)
{
var user = Services.UserService.GetByEmail(email);
if (user != null && (extraCheck == null || extraCheck(user)))
{
ModelState.AddModelError("Email", "A user with the email already exists");
throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState));
}
return user;
}
private IUser CheckUniqueUsername(string username, Func<IUser, bool> extraCheck)
{
var user = Services.UserService.GetByUsername(username);
if (user != null && (extraCheck == null || extraCheck(user)))
{
ModelState.AddModelError(
UmbracoConfig.For.UmbracoSettings().Security.UsernameIsEmail ? "Email" : "Username",
"A user with the username already exists");
throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState));
}
return user;
}
private HttpContextBase EnsureHttpContext()
{
@@ -373,12 +451,15 @@ namespace Umbraco.Web.Editors
UserExtensions.GetUserCulture(to.Language, Services.TextService),
new[] { userDisplay.Name, from, message, inviteUri.ToString() });
await UserManager.EmailService.SendAsync(new IdentityMessage
{
Body = emailBody,
Destination = userDisplay.Email,
Subject = emailSubject
});
await UserManager.EmailService.SendAsync(
//send the special UmbracoEmailMessage which configures it's own sender
//to allow for events to handle sending the message if no smtp is configured
new UmbracoEmailMessage(new EmailSender(true))
{
Body = emailBody,
Destination = userDisplay.Email,
Subject = emailSubject
});
}
@@ -404,6 +485,14 @@ namespace Umbraco.Web.Editors
if (found == null)
throw new HttpResponseException(HttpStatusCode.NotFound);
//Perform authorization here to see if the current user can actually save this user with the info being requested
var authHelper = new UserEditorAuthorizationHelper(Services.ContentService, Services.MediaService, Services.UserService, Services.EntityService);
var canSaveUser = authHelper.IsAuthorized(Security.CurrentUser, found, userSave.StartContentIds, userSave.StartMediaIds, userSave.UserGroups);
if (canSaveUser == false)
{
throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.Unauthorized, canSaveUser.Result));
}
var hasErrors = false;
var existing = Services.UserService.GetByEmail(userSave.Email);
@@ -440,23 +529,24 @@ namespace Umbraco.Web.Editors
userSave.Username = userSave.Email;
}
var resetPasswordValue = string.Empty;
if (userSave.ChangePassword != null)
{
var passwordChanger = new PasswordChanger(Logger, Services.UserService);
var passwordChangeResult = await passwordChanger.ChangePasswordWithIdentityAsync(found, userSave.ChangePassword, ModelState, UserManager);
var passwordChangeResult = await passwordChanger.ChangePasswordWithIdentityAsync(Security.CurrentUser, found, userSave.ChangePassword, UserManager);
if (passwordChangeResult.Success)
{
//depending on how the provider is configured, the password may be reset so let's store that for later
resetPasswordValue = passwordChangeResult.Result.ResetPassword;
//need to re-get the user
//need to re-get the user
found = Services.UserService.GetUserById(intId.Result);
}
else
{
hasErrors = true;
foreach (var memberName in passwordChangeResult.Result.ChangeError.MemberNames)
{
ModelState.AddModelError(memberName, passwordChangeResult.Result.ChangeError.ErrorMessage);
}
}
}
@@ -470,13 +560,9 @@ namespace Umbraco.Web.Editors
var display = Mapper.Map<UserDisplay>(user);
//re-map the password reset value (if any)
if (resetPasswordValue.IsNullOrWhiteSpace() == false)
display.ResetPasswordValue = resetPasswordValue;
display.AddSuccessNotification(Services.TextService.Localize("speechBubbles/operationSavedHeader"), Services.TextService.Localize("speechBubbles/editUserSaved"));
return display;
}
}
/// <summary>
/// Disables the users with the given user ids
@@ -528,7 +614,43 @@ namespace Umbraco.Web.Editors
}
return Request.CreateNotificationSuccessResponse(
Services.TextService.Localize("speechBubbles/enableUserSuccess", new[] { users[0].Name }));
Services.TextService.Localize("speechBubbles/enableUserSuccess", new[] { users[0].Name }));
}
/// <summary>
/// Unlocks the users with the given user ids
/// </summary>
/// <param name="userIds"></param>
public async Task<HttpResponseMessage> PostUnlockUsers([FromUri]int[] userIds)
{
if (userIds.Length <= 0)
return Request.CreateResponse(HttpStatusCode.OK);
if (userIds.Length == 1)
{
var unlockResult = await UserManager.SetLockoutEndDateAsync(userIds[0], DateTimeOffset.Now);
if (unlockResult.Succeeded == false)
{
return Request.CreateValidationErrorResponse(
string.Format("Could not unlock for user {0} - error {1}", userIds[0], unlockResult.Errors.First()));
}
var user = await UserManager.FindByIdAsync(userIds[0]);
return Request.CreateNotificationSuccessResponse(
Services.TextService.Localize("speechBubbles/unlockUserSuccess", new[] { user.Name }));
}
foreach (var u in userIds)
{
var unlockResult = await UserManager.SetLockoutEndDateAsync(u, DateTimeOffset.Now);
if (unlockResult.Succeeded == false)
{
return Request.CreateValidationErrorResponse(
string.Format("Could not unlock for user {0} - error {1}", u, unlockResult.Errors.First()));
}
}
return Request.CreateNotificationSuccessResponse(
Services.TextService.Localize("speechBubbles/unlockUsersSuccess", new[] { userIds.Length.ToString() }));
}
public HttpResponseMessage PostSetUserGroupsOnUsers([FromUri]string[] userGroupAliases, [FromUri]int[] userIds)
@@ -561,5 +683,6 @@ namespace Umbraco.Web.Editors
[DataMember(Name = "userStates")]
public IDictionary<UserState, int> UserStates { get; set; }
}
}
}
@@ -30,8 +30,7 @@ namespace Umbraco.Web.HealthCheck.Checks.Config
{
get
{
return TextService.Localize("healthcheck/customErrorsCheckSuccessMessage",
new[] { Values.First(v => v.IsRecommended).Value });
return TextService.Localize("healthcheck/customErrorsCheckSuccessMessage", new[] { CurrentValue });
}
}
@@ -59,12 +59,10 @@ namespace Umbraco.Web.HealthCheck.Checks.Permissions
// in ALL circumstances or just some
var pathsToCheck = new Dictionary<string, PermissionCheckRequirement>
{
{ SystemDirectories.AppCode, PermissionCheckRequirement.Optional },
{ SystemDirectories.Data, PermissionCheckRequirement.Required },
{ SystemDirectories.Packages, PermissionCheckRequirement.Required},
{ SystemDirectories.Preview, PermissionCheckRequirement.Required },
{ SystemDirectories.AppPlugins, PermissionCheckRequirement.Required },
{ SystemDirectories.Bin, PermissionCheckRequirement.Optional },
{ SystemDirectories.Config, PermissionCheckRequirement.Optional },
{ SystemDirectories.Css, PermissionCheckRequirement.Optional },
{ SystemDirectories.Masterpages, PermissionCheckRequirement.Optional },
@@ -77,11 +75,32 @@ namespace Umbraco.Web.HealthCheck.Checks.Permissions
{ SystemDirectories.Xslt, PermissionCheckRequirement.Optional },
};
//These are special paths to check that will restart an app domain if a file is written to them,
//so these need to be tested differently
var pathsToCheckWithRestarts = new Dictionary<string, PermissionCheckRequirement>
{
{ SystemDirectories.AppCode, PermissionCheckRequirement.Optional },
{ SystemDirectories.Bin, PermissionCheckRequirement.Optional }
};
// Run checks for required and optional paths for modify permission
IEnumerable<string> requiredFailedPaths;
IEnumerable<string> optionalFailedPaths;
var requiredPathCheckResult = FilePermissionHelper.EnsureDirectories(GetPathsToCheck(pathsToCheck, PermissionCheckRequirement.Required), out requiredFailedPaths);
var optionalPathCheckResult = FilePermissionHelper.EnsureDirectories(GetPathsToCheck(pathsToCheck, PermissionCheckRequirement.Optional), out optionalFailedPaths);
var requiredPathCheckResult = FilePermissionHelper.EnsureDirectories(
GetPathsToCheck(pathsToCheck, PermissionCheckRequirement.Required), out var requiredFailedPaths);
var optionalPathCheckResult = FilePermissionHelper.EnsureDirectories(
GetPathsToCheck(pathsToCheck, PermissionCheckRequirement.Optional), out var optionalFailedPaths);
//now check the special folders
var requiredPathCheckResult2 = FilePermissionHelper.EnsureDirectories(
GetPathsToCheck(pathsToCheckWithRestarts, PermissionCheckRequirement.Required), out var requiredFailedPaths2, writeCausesRestart:true);
var optionalPathCheckResult2 = FilePermissionHelper.EnsureDirectories(
GetPathsToCheck(pathsToCheckWithRestarts, PermissionCheckRequirement.Optional), out var optionalFailedPaths2, writeCausesRestart: true);
requiredPathCheckResult = requiredPathCheckResult && requiredPathCheckResult2;
optionalPathCheckResult = optionalPathCheckResult && optionalPathCheckResult2;
//combine the paths
requiredFailedPaths = requiredFailedPaths.Concat(requiredFailedPaths2).ToList();
optionalFailedPaths = requiredFailedPaths.Concat(optionalFailedPaths2).ToList();
return GetStatus(requiredPathCheckResult, requiredFailedPaths, optionalPathCheckResult, optionalFailedPaths, PermissionCheckFor.Folder);
}
@@ -50,17 +50,10 @@ namespace Umbraco.Web.HealthCheck.NotificationMethods
var subject = _textService.Localize("healthcheck/scheduledHealthCheckEmailSubject");
using (var client = new SmtpClient())
var mailSender = new EmailSender();
using (var mailMessage = CreateMailMessage(subject, message))
{
if (client.DeliveryMethod == SmtpDeliveryMethod.Network)
{
await client.SendMailAsync(mailMessage);
}
else
{
client.Send(mailMessage);
}
await mailSender.SendAsync(mailMessage);
}
}
@@ -5,6 +5,7 @@ namespace Umbraco.Web.HealthCheck.NotificationMethods
{
public interface IHealthCheckNotificationMethod
{
bool Enabled { get; }
Task SendAsync(HealthCheckResults results, CancellationToken token);
}
}
+68 -6
View File
@@ -3,8 +3,10 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using HtmlAgilityPack;
using Umbraco.Web.WebApi.Filters;
namespace Umbraco.Web
{
@@ -20,13 +22,14 @@ namespace Umbraco.Web
/// <returns>The text with text line breaks replaced with html linebreaks (<br/>)</returns>
public string ReplaceLineBreaksForHtml(string text)
{
return text.Replace("\n", "<br/>\n");
return text.Replace("\r\n", @"<br />").Replace("\n", @"<br />").Replace("\r", @"<br />");
}
public HtmlString StripHtmlTags(string html, params string[] tags)
{
var doc = new HtmlDocument();
doc.LoadHtml("<p>" + html + "</p>");
var targets = new List<HtmlNode>();
var nodes = doc.DocumentNode.FirstChild.SelectNodes(".//*");
@@ -52,7 +55,7 @@ namespace Umbraco.Web
{
return new HtmlString(html);
}
return new HtmlString(doc.DocumentNode.FirstChild.InnerHtml);
return new HtmlString(doc.DocumentNode.FirstChild.InnerHtml.Replace(" ", " "));
}
internal string Join(string separator, params object[] args)
@@ -89,6 +92,8 @@ namespace Umbraco.Web
public IHtmlString Truncate(string html, int length, bool addElipsis, bool treatTagsAsContent)
{
const string hellip = "&hellip;";
using (var outputms = new MemoryStream())
{
using (var outputtw = new StreamWriter(outputms))
@@ -110,7 +115,7 @@ namespace Umbraco.Web
isTagClose = false;
int ic = 0,
currentLength = 0,
//currentLength = 0,
currentTextLength = 0;
string currentTag = string.Empty,
@@ -145,6 +150,10 @@ namespace Umbraco.Web
{
string thisTag = tagStack.Pop();
outputtw.Write("</" + thisTag + ">");
if (treatTagsAsContent)
{
currentTextLength++;
}
}
if (!isTagClose && currentTag.Length > 0)
{
@@ -152,6 +161,10 @@ namespace Umbraco.Web
{
tagStack.Push(currentTag);
outputtw.Write("<" + currentTag);
if (treatTagsAsContent)
{
currentTextLength++;
}
if (!string.IsNullOrEmpty(tagContents))
{
if (tagContents.EndsWith("/"))
@@ -209,7 +222,7 @@ namespace Umbraco.Web
{
var charToWrite = (char)ic;
outputtw.Write(charToWrite);
currentLength++;
//currentLength++;
}
}
@@ -225,7 +238,7 @@ namespace Umbraco.Web
// Reached truncate limit.
if (addElipsis)
{
outputtw.Write("&hellip;");
outputtw.Write(hellip);
}
lengthReached = true;
}
@@ -239,10 +252,59 @@ namespace Umbraco.Web
outputms.Position = 0;
using (TextReader outputtr = new StreamReader(outputms))
{
return new HtmlString(outputtr.ReadToEnd().Replace(" ", " ").Trim());
string result = string.Empty;
string firstTrim = outputtr.ReadToEnd().Replace(" ", " ").Trim();
//Check to see if there is an empty char between the hellip and the output string
//if there is, remove it
if (string.IsNullOrWhiteSpace(firstTrim) == false)
{
result = firstTrim[firstTrim.Length - hellip.Length - 1] == ' ' ? firstTrim.Remove(firstTrim.Length - hellip.Length - 1, 1) : firstTrim;
}
return new HtmlString(result);
}
}
}
}
/// <summary>
/// Returns the length of the words from a html block
/// </summary>
/// <param name="html">Html text</param>
/// <param name="words">Amount of words you would like to measure</param>
/// <param name="tagsAsContent"></param>
/// <returns></returns>
public int WordsToLength(string html, int words)
{
HtmlDocument doc = new HtmlDocument();
doc.LoadHtml(html);
int wordCount = 0,
length = 0,
maxWords = words;
html = StripHtmlTags(html, null).ToString();
while (length < html.Length)
{
// Check to see if the current wordCount reached the maxWords allowed
if (wordCount.Equals(maxWords)) break;
// Check if current char is part of a word
while (length < html.Length && char.IsWhiteSpace(html[length]) == false)
{
length++;
}
wordCount++;
// Skip whitespace until the next word
while (length < html.Length && char.IsWhiteSpace(html[length]) && wordCount.Equals(maxWords) == false)
{
length++;
}
}
return length;
}
}
}
+34
View File
@@ -16,6 +16,40 @@ namespace Umbraco.Web
/// </remarks>
internal static class HttpCookieExtensions
{
/// <summary>
/// Retrieves an individual cookie from the cookies collection
/// </summary>
/// <param name="requestHeaders"></param>
/// <param name="cookieName"></param>
/// <returns></returns>
/// <remarks>
/// Adapted from: https://stackoverflow.com/a/29057304/5018 because there's an issue with .NET WebApi cookie parsing logic
/// when using requestHeaders.GetCookies() when an invalid cookie name is present.
/// </remarks>
public static string GetCookieValue(this HttpRequestHeaders requestHeaders, string cookieName)
{
foreach (var header in requestHeaders)
{
if (header.Key.Equals("Cookie", StringComparison.InvariantCultureIgnoreCase) == false)
continue;
var cookiesHeaderValue = header.Value.FirstOrDefault();
if (cookiesHeaderValue == null)
return null;
var cookieCollection = cookiesHeaderValue.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var cookieNameValue in cookieCollection)
{
var parts = cookieNameValue.Split(new[] { '=' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length != 2) continue;
if (parts[0].Trim().Equals(cookieName, StringComparison.InvariantCultureIgnoreCase))
return parts[1].Trim();
}
}
return null;
}
/// <summary>
/// Removes the cookie from the request and the response if it exists
/// </summary>
@@ -40,6 +40,8 @@ namespace Umbraco.Web.Install.Controllers
// Update ClientDependency version
var clientDependencyConfig = new ClientDependencyConfiguration(_logger);
var clientDependencyUpdated = clientDependencyConfig.IncreaseVersionNumber();
// Delete ClientDependency temp directories to make sure we get fresh caches
var clientDependencyTempFilesDeleted = clientDependencyConfig.ClearTempFiles(HttpContext);
var result = _umbracoContext.Security.ValidateCurrentUser(false);
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Security.AccessControl;
using Umbraco.Core.IO;
using Umbraco.Web.Composing;
using Umbraco.Web.PublishedCache;
@@ -41,7 +42,18 @@ namespace Umbraco.Web.Install
return report.Count == 0;
}
public static bool EnsureDirectories(string[] dirs, out IEnumerable<string> errors)
/// <summary>
/// This will test the directories for write access
/// </summary>
/// <param name="directories"></param>
/// <param name="errorReport"></param>
/// <param name="writeCausesRestart">
/// If this is false, the easiest way to test for write access is to write a temp file, however some folder will cause
/// an App Domain restart if a file is written to the folder, so in that case we need to use the ACL APIs which aren't as
/// reliable but we cannot write a file since it will cause an app domain restart.
/// </param>
/// <returns></returns>
public static bool EnsureDirectories(string[] dirs, out IEnumerable<string> errors, bool writeCausesRestart = false)
{
List<string> temp = null;
var success = true;
@@ -49,7 +61,7 @@ namespace Umbraco.Web.Install
{
// we don't want to create/ship unnecessary directories, so
// here we just ensure we can access the directory, not create it
var tryAccess = TryAccessDirectory(dir);
var tryAccess = TryAccessDirectory(dir, !writeCausesRestart);
if (tryAccess) continue;
if (temp == null) temp = new List<string>();
@@ -151,8 +163,13 @@ namespace Umbraco.Web.Install
// tries to create a file
// if successful, the file is deleted
//
// or
//
// use the ACL APIs to avoid creating files
//
// if the directory does not exist, do nothing & success
public static bool TryAccessDirectory(string dir)
public static bool TryAccessDirectory(string dir, bool canWrite)
{
try
{
@@ -161,10 +178,17 @@ namespace Umbraco.Web.Install
if (Directory.Exists(dirPath) == false)
return true;
var filePath = dirPath + "/" + CreateRandomName() + ".tmp";
File.WriteAllText(filePath, "This is an Umbraco internal test file. It is safe to delete it.");
File.Delete(filePath);
return true;
if (canWrite)
{
var filePath = dirPath + "/" + CreateRandomName() + ".tmp";
File.WriteAllText(filePath, "This is an Umbraco internal test file. It is safe to delete it.");
File.Delete(filePath);
return true;
}
else
{
return HasWritePermission(dirPath);
}
}
catch
{
@@ -172,6 +196,42 @@ namespace Umbraco.Web.Install
}
}
private static bool HasWritePermission(string path)
{
var writeAllow = false;
var writeDeny = false;
var accessControlList = Directory.GetAccessControl(path);
if (accessControlList == null)
return false;
AuthorizationRuleCollection accessRules;
try
{
accessRules = accessControlList.GetAccessRules(true, true, typeof(System.Security.Principal.SecurityIdentifier));
if (accessRules == null)
return false;
}
catch (Exception e)
{
//This is not 100% accurate btw because it could turn out that the current user doesn't
//have access to read the current permissions but does have write access.
//I think this is an edge case however
return false;
}
foreach (FileSystemAccessRule rule in accessRules)
{
if ((FileSystemRights.Write & rule.FileSystemRights) != FileSystemRights.Write)
continue;
if (rule.AccessControlType == AccessControlType.Allow)
writeAllow = true;
else if (rule.AccessControlType == AccessControlType.Deny)
writeDeny = true;
}
return writeAllow && writeDeny == false;
}
// tries to write into a file
// fails if the directory does not exist
private static bool TryWriteFile(string file)
@@ -20,8 +20,21 @@ namespace Umbraco.Web.Models
public string OldPassword { get; set; }
/// <summary>
/// Set to true if the password is to be reset - only valid when: EnablePasswordReset = true
/// Set to true if the password is to be reset
/// </summary>
/// <remarks>
/// <para>
/// This operator is different between using ASP.NET Identity APIs and Membership APIs.
/// </para>
/// <para>
/// When using Membership APIs, this is only valid when: EnablePasswordReset = true and it will reset the password to something auto generated.
/// </para>
/// <para>
/// When using ASP.NET Identity APIs this needs to be set if an administrator user that has access to the Users section is changing another users
/// password. This flag is required to indicate that the oldPassword value is not required and that we are in fact performing a password reset and
/// then a password change if the executing user has access to do so.
/// </para>
/// </remarks>
[DataMember(Name = "reset")]
public bool? Reset { get; set; }
@@ -1,15 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http.Formatting;
using System.Runtime.Serialization;
using System.Web.Http;
using System.Web.Http.ModelBinding;
using Umbraco.Core;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Validation;
using Umbraco.Web.Models.Trees;
using Umbraco.Web.Trees;
namespace Umbraco.Web.Models.ContentEditing
{
@@ -57,6 +49,8 @@ namespace Umbraco.Web.Models.ContentEditing
/// </remarks>
[DataMember(Name = "allowedActions")]
public IEnumerable<string> AllowedActions { get; set; }
[DataMember(Name = "isBlueprint")]
public bool IsBlueprint { get; set; }
}
}
@@ -1,20 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.Serialization;
namespace Umbraco.Web.Models.ContentEditing
{
/// <summary>
/// Represents a section (application) in the back office
/// </summary>
[DataContract(Name = "section", Namespace = "")]
public class Section
{
[DataMember(Name = "name")]
public string Name { get; set; }
@@ -24,5 +17,11 @@ namespace Umbraco.Web.Models.ContentEditing
[DataMember(Name = "alias")]
public string Alias { get; set; }
/// <summary>
/// In some cases a custom route path can be specified so that when clicking on a section it goes to this
/// path instead of the normal dashboard path
/// </summary>
[DataMember(Name = "routePath")]
public string RoutePath { get; set; }
}
}
@@ -27,10 +27,14 @@ namespace Umbraco.Web.Models.ContentEditing
public string EmailHash { 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)]
[EditorBrowsable(EditorBrowsableState.Never)]
[ReadOnly(true)]
[DataMember(Name = "userType")]
public string UserType { get; set; }
public string UserType { get; set; }
[ReadOnly(true)]
[DataMember(Name = "userGroups")]
public string[] UserGroups { get; set; }
/// <summary>
/// Gets/sets the number of seconds for the user's auth ticket to expire
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Runtime.Serialization;
@@ -38,6 +39,40 @@ namespace Umbraco.Web.Models.ContentEditing
[DataMember(Name = "resetPasswordValue")]
[ReadOnly(true)]
public string ResetPasswordValue { get; set; }
/// <summary>
/// A readonly value showing the user's current calculated start content ids
/// </summary>
[DataMember(Name = "calculatedStartContentIds")]
[ReadOnly(true)]
public IEnumerable<EntityBasic> CalculatedStartContentIds { get; set; }
/// <summary>
/// A readonly value showing the user's current calculated start media ids
/// </summary>
[DataMember(Name = "calculatedStartMediaIds")]
[ReadOnly(true)]
public IEnumerable<EntityBasic> CalculatedStartMediaIds { get; set; }
[DataMember(Name = "failedPasswordAttempts")]
[ReadOnly(true)]
public int FailedPasswordAttempts { get; set; }
[DataMember(Name = "lastLockoutDate")]
[ReadOnly(true)]
public DateTime LastLockoutDate { get; set; }
[DataMember(Name = "lastPasswordChangeDate")]
[ReadOnly(true)]
public DateTime LastPasswordChangeDate { get; set; }
[DataMember(Name = "createDate")]
[ReadOnly(true)]
public DateTime CreateDate { get; set; }
[DataMember(Name = "updateDate")]
[ReadOnly(true)]
public DateTime UpdateDate { get; set; }
}
}
@@ -2,6 +2,8 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Runtime.Serialization;
using Umbraco.Core;
using Umbraco.Core.Configuration;
namespace Umbraco.Web.Models.ContentEditing
{
@@ -18,7 +20,10 @@ namespace Umbraco.Web.Models.ContentEditing
[DataMember(Name = "email", IsRequired = true)]
[Required]
[EmailAddress]
public string Email { get; set; }
public string Email { get; set; }
[DataMember(Name = "username")]
public string Username { get; set; }
[DataMember(Name = "message")]
public string Message { get; set; }
@@ -26,7 +31,10 @@ namespace Umbraco.Web.Models.ContentEditing
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (UserGroups.Any() == false)
yield return new ValidationResult("A user must be assigned to at least one group", new[] { "UserGroups" });
yield return new ValidationResult("A user must be assigned to at least one group", new[] { "UserGroups" });
if (UmbracoConfig.For.UmbracoSettings().Security.UsernameIsEmail == false && Username.IsNullOrWhiteSpace())
yield return new ValidationResult("A username cannot be empty", new[] { "Username" });
}
}
}
+3 -2
View File
@@ -2,6 +2,7 @@
using System.ComponentModel.DataAnnotations;
using System.Web;
using Umbraco.Core;
using Umbraco.Web.Composing;
using Umbraco.Web.Security;
namespace Umbraco.Web.Models
@@ -22,9 +23,9 @@ namespace Umbraco.Web.Models
private LoginStatusModel(bool doLookup)
{
if (doLookup && HttpContext.Current != null)
if (doLookup && Current.UmbracoContext != null)
{
var helper = new MembershipHelper(new HttpContextWrapper(HttpContext.Current));
var helper = new MembershipHelper(Current.UmbracoContext);
var model = helper.GetCurrentLoginStatus();
if (model != null)
{
@@ -10,8 +10,8 @@ using Umbraco.Core.Models;
using Umbraco.Core.Services;
using Umbraco.Web.Composing;
using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Trees;
using Umbraco.Web.Routing;
using Umbraco.Web.Trees;
using Umbraco.Web._Legacy.Actions;
namespace Umbraco.Web.Models.Mapping
@@ -31,7 +31,8 @@ namespace Umbraco.Web.Models.Mapping
//FROM IContent TO ContentItemDisplay
CreateMap<IContent, ContentItemDisplay>()
.ForMember(dest => dest.Udi, opt => opt.MapFrom(src => Udi.Create(Constants.UdiEntityType.Document, src.Key)))
.ForMember(dest => dest.Udi, opt => opt.MapFrom(src =>
Udi.Create(src.IsBlueprint ? Constants.UdiEntityType.DocumentBluePrint : Constants.UdiEntityType.Document, src.Key)))
.ForMember(dest => dest.Owner, opt => opt.ResolveUsing(src => contentOwnerResolver.Resolve(src)))
.ForMember(dest => dest.Updater, opt => opt.ResolveUsing(src => creatorResolver.Resolve(src)))
.ForMember(dest => dest.Icon, opt => opt.MapFrom(src => src.ContentType.Icon))
@@ -59,7 +60,8 @@ namespace Umbraco.Web.Models.Mapping
//FROM IContent TO ContentItemBasic<ContentPropertyBasic, IContent>
CreateMap<IContent, ContentItemBasic<ContentPropertyBasic, IContent>>()
.ForMember(dest => dest.Udi, opt => opt.MapFrom(src => Udi.Create(Constants.UdiEntityType.Document, src.Key)))
.ForMember(dest => dest.Udi, opt => opt.MapFrom(src =>
Udi.Create(src.IsBlueprint ? Constants.UdiEntityType.DocumentBluePrint : Constants.UdiEntityType.Document, src.Key)))
.ForMember(dest => dest.Owner, opt => opt.ResolveUsing(src => contentOwnerResolver.Resolve(src)))
.ForMember(dest => dest.Updater, opt => opt.ResolveUsing(src => creatorResolver.Resolve(src)))
.ForMember(dest => dest.Icon, opt => opt.MapFrom(src => src.ContentType.Icon))
@@ -70,7 +72,8 @@ namespace Umbraco.Web.Models.Mapping
//FROM IContent TO ContentItemDto<IContent>
CreateMap<IContent, ContentItemDto<IContent>>()
.ForMember(dest => dest.Udi, opt => opt.MapFrom(src => Udi.Create(Constants.UdiEntityType.Document, src.Key)))
.ForMember(dest => dest.Udi, opt => opt.MapFrom(src =>
Udi.Create(src.IsBlueprint ? Constants.UdiEntityType.DocumentBluePrint : Constants.UdiEntityType.Document, src.Key)))
.ForMember(dest => dest.Owner, opt => opt.ResolveUsing(src => contentOwnerResolver.Resolve(src)))
.ForMember(dest => dest.HasPublishedVersion, opt => opt.MapFrom(src => src.HasPublishedVersion))
.ForMember(dest => dest.Updater, opt => opt.Ignore())
@@ -14,9 +14,8 @@ namespace Umbraco.Web.Models.Mapping
_textService = textService;
CreateMap<Core.Models.Section, Section>()
.ForMember(
dto => dto.Name,
expression => expression.MapFrom(section => _textService.Localize("sections/" + section.Alias, (IDictionary<string, string>)null)))
.ForMember(dest => dest.RoutePath, opt => opt.Ignore())
.ForMember(dest => dest.Name, opt => opt.MapFrom(src => _textService.Localize("sections/" + src.Alias, (IDictionary<string, string>)null)))
.ReverseMap(); //backwards too!
}
}
+44 -38
View File
@@ -223,13 +223,33 @@ namespace Umbraco.Web.Models.Mapping
display.AssignedPermissions = allAssignedPermissions;
});
//Important! Currently we are never mapping to multiple UserDisplay objects but if we start doing that
// this will cause an N+1 and we'll need to change how this works.
CreateMap<IUser, UserDisplay>()
.ForMember(dest => dest.Avatars, opt => opt.MapFrom(user => user.GetCurrentUserAvatarUrls(userService, runtimeCache)))
.ForMember(dest => dest.Username, opt => opt.MapFrom(user => user.Username))
.ForMember(dest => dest.LastLoginDate, opt => opt.MapFrom(user => user.LastLoginDate == default(DateTime) ? null : (DateTime?)user.LastLoginDate))
.ForMember(dest => dest.UserGroups, opt => opt.MapFrom(user => user.Groups))
.ForMember(dest => dest.StartContentIds, opt => opt.UseValue(Enumerable.Empty<EntityBasic>()))
.ForMember(dest => dest.StartMediaIds, opt => opt.UseValue(Enumerable.Empty<EntityBasic>()))
.ForMember(
dest => dest.CalculatedStartContentIds,
opt => opt.MapFrom(src => GetStartNodeValues(
src.CalculateContentStartNodeIds(entityService),
textService, entityService, UmbracoObjectTypes.Document, "content/contentRoot")))
.ForMember(
dest => dest.CalculatedStartMediaIds,
opt => opt.MapFrom(src => GetStartNodeValues(
src.CalculateMediaStartNodeIds(entityService),
textService, entityService, UmbracoObjectTypes.Media, "media/mediaRoot")))
.ForMember(
dest => dest.StartContentIds,
opt => opt.MapFrom(src => GetStartNodeValues(
src.StartContentIds.ToArray(),
textService, entityService, UmbracoObjectTypes.Document, "content/contentRoot")))
.ForMember(
dest => dest.StartMediaIds,
opt => opt.MapFrom(src => GetStartNodeValues(
src.StartMediaIds.ToArray(),
textService, entityService, UmbracoObjectTypes.Media, "media/mediaRoot")))
.ForMember(dest => dest.Culture, opt => opt.MapFrom(user => user.GetUserCulture(textService)))
.ForMember(
dest => dest.AvailableCultures,
@@ -247,40 +267,7 @@ namespace Umbraco.Web.Models.Mapping
.ForMember(dest => dest.ResetPasswordValue, opt => opt.Ignore())
.ForMember(dest => dest.Alias, opt => opt.Ignore())
.ForMember(dest => dest.Trashed, opt => opt.Ignore())
.ForMember(dest => dest.AdditionalData, opt => opt.Ignore())
.AfterMap((user, display) =>
{
//Important! Currently we are never mapping to multiple UserDisplay objects but if we start doing that
// this will cause an N+1 and we'll need to change how this works.
var startContentIds = user.StartContentIds.ToArray();
if (startContentIds.Length > 0)
{
//TODO: Update GetAll to be able to pass in a parameter like on the normal Get to NOT load in the entire object!
var startNodes = new List<EntityBasic>();
if (startContentIds.Contains(-1))
{
startNodes.Add(RootNode(textService.Localize("content/contentRoot")));
}
var contentItems = entityService.GetAll(UmbracoObjectTypes.Document, startContentIds);
startNodes.AddRange(Mapper.Map<IEnumerable<IUmbracoEntity>, IEnumerable<EntityBasic>>(contentItems));
display.StartContentIds = startNodes;
}
var startMediaIds = user.StartMediaIds.ToArray();
if (startMediaIds.Length > 0)
{
var startNodes = new List<EntityBasic>();
if (startContentIds.Contains(-1))
{
startNodes.Add(RootNode(textService.Localize("media/mediaRoot")));
}
var mediaItems = entityService.GetAll(UmbracoObjectTypes.Media, startMediaIds);
startNodes.AddRange(Mapper.Map<IEnumerable<IUmbracoEntity>, IEnumerable<EntityBasic>>(mediaItems));
display.StartMediaIds = startNodes;
}
});
.ForMember(dest => dest.AdditionalData, opt => opt.Ignore());
CreateMap<IUser, UserBasic>()
//Loading in the user avatar's requires an external request if they don't have a local file avatar, this means that initial load of paging may incur a cost
@@ -317,20 +304,23 @@ namespace Umbraco.Web.Models.Mapping
dest => dest.EmailHash,
opt => opt.MapFrom(user => user.Email.ToLowerInvariant().Trim().GenerateHash()))
.ForMember(dest => dest.SecondsUntilTimeout, opt => opt.Ignore())
.ForMember(dest => dest.UserGroups, opt => opt.Ignore())
.AfterMap((user, detail) =>
{
//we need to map the legacy UserType
//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 = user.Groups.ToArray();
detail.UserGroups = user.Groups.Select(x => x.Alias).ToArray();
if (groups.Length == 0)
{
//In backwards compatibility land, a user type cannot be null! so we need to return a fake one.
//In backwards compatibility land, a user type cannot be null! so we need to return a fake one.
detail.UserType = "temp";
}
else
{
var builtIns = new[] { Constants.Security.AdminGroupAlias, "writer", "editor", "translator" };
var builtIns = new[] { Constants.Security.AdminGroupAlias, "writer", "editor", Constants.Security.TranslatorGroupAlias };
var foundBuiltIn = groups.FirstOrDefault(x => builtIns.Contains(x.Alias));
if (foundBuiltIn != null)
{
@@ -361,6 +351,22 @@ namespace Umbraco.Web.Models.Mapping
.ForMember(dest => dest.SessionId, opt => opt.MapFrom(user => user.SecurityStamp.IsNullOrWhiteSpace() ? Guid.NewGuid().ToString("N") : user.SecurityStamp));
}
private IEnumerable<EntityBasic> GetStartNodeValues(int[] startNodeIds,
ILocalizedTextService textService, IEntityService entityService, UmbracoObjectTypes objectType,
string localizedKey)
{
if (startNodeIds.Length <= 0)
return Enumerable.Empty<EntityBasic>();
var startNodes = new List<EntityBasic>();
if (startNodeIds.Contains(-1))
startNodes.Add(RootNode(textService.Localize(localizedKey)));
var mediaItems = entityService.GetAll(objectType, startNodeIds);
startNodes.AddRange(Mapper.Map<IEnumerable<IUmbracoEntity>, IEnumerable<EntityBasic>>(mediaItems));
return startNodes;
}
private void MapUserGroupBasic(ISectionService sectionService, IEntityService entityService, ILocalizedTextService textService, dynamic group, UserGroupBasic display)
{
var allSections = sectionService.GetSections();
+3 -2
View File
@@ -4,6 +4,7 @@ using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Web;
using System.Web.Mvc;
using Umbraco.Web.Composing;
using Umbraco.Web.Security;
namespace Umbraco.Web.Models
@@ -24,9 +25,9 @@ namespace Umbraco.Web.Models
private ProfileModel(bool doLookup)
{
MemberProperties = new List<UmbracoProperty>();
if (doLookup)
if (doLookup && Current.UmbracoContext != null)
{
var helper = new MembershipHelper(new HttpContextWrapper(HttpContext.Current));
var helper = new MembershipHelper(Current.UmbracoContext);
var model = helper.GetCurrentMemberProfileModel();
MemberProperties = model.MemberProperties;
}
+3 -2
View File
@@ -5,6 +5,7 @@ using System.ComponentModel.DataAnnotations;
using System.Web;
using System.Web.Mvc;
using Umbraco.Core;
using Umbraco.Web.Composing;
using Umbraco.Web.Security;
namespace Umbraco.Web.Models
@@ -29,9 +30,9 @@ namespace Umbraco.Web.Models
MemberProperties = new List<UmbracoProperty>();
LoginOnSuccess = true;
CreatePersistentLoginCookie = true;
if (doLookup && HttpContext.Current != null)
if (doLookup && Current.UmbracoContext != null)
{
var helper = new MembershipHelper(new HttpContextWrapper(HttpContext.Current));
var helper = new MembershipHelper(Current.UmbracoContext);
var model = helper.CreateRegistrationModel(MemberTypeAlias);
MemberProperties = model.MemberProperties;
}
@@ -10,8 +10,14 @@ namespace Umbraco.Web.PropertyEditors
/// <summary>
/// The constructor will setup the property editor based on the attribute if one is found
/// </summary>
public TextAreaPropertyEditor(ILogger logger) : base(logger)
public TextAreaPropertyEditor(ILogger logger)
: base(logger)
{ }
protected override PropertyValueEditor CreateValueEditor()
{
return new TextOnlyValueEditor(base.CreateValueEditor());
}
protected override PreValueEditor CreatePreValueEditor()
{
@@ -0,0 +1,46 @@
using System;
using Umbraco.Core.Models;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Services;
namespace Umbraco.Web.PropertyEditors
{
/// <summary>
/// Custom value editor which ensures that the value stored is just plain text and that
/// no magic json formatting occurs when translating it to and from the database values
/// </summary>
public class TextOnlyValueEditor : PropertyValueEditorWrapper
{
public TextOnlyValueEditor(PropertyValueEditor wrapped) : base(wrapped)
{
}
/// <summary>
/// A method used to format the database value to a value that can be used by the editor
/// </summary>
/// <param name="property"></param>
/// <param name="propertyType"></param>
/// <param name="dataTypeService"></param>
/// <returns></returns>
/// <remarks>
/// The object returned will always be a string and if the database type is not a valid string type an exception is thrown
/// </remarks>
public override object ConvertDbToEditor(Property property, PropertyType propertyType, IDataTypeService dataTypeService)
{
if (property.Value == null) return string.Empty;
switch (GetDatabaseType())
{
case DataTypeDatabaseType.Ntext:
case DataTypeDatabaseType.Nvarchar:
return property.Value.ToString();
case DataTypeDatabaseType.Integer:
case DataTypeDatabaseType.Decimal:
case DataTypeDatabaseType.Date:
default:
throw new InvalidOperationException("The " + typeof(TextOnlyValueEditor) + " can only be used with string based property editors");
}
}
}
}
@@ -1,13 +1,12 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Umbraco.Core;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Services;
namespace Umbraco.Web.PropertyEditors
{
@@ -17,8 +16,14 @@ namespace Umbraco.Web.PropertyEditors
/// <summary>
/// The constructor will setup the property editor based on the attribute if one is found
/// </summary>
public TextboxPropertyEditor(ILogger logger) : base(logger)
public TextboxPropertyEditor(ILogger logger)
: base(logger)
{ }
protected override PropertyValueEditor CreateValueEditor()
{
return new TextOnlyValueEditor(base.CreateValueEditor());
}
protected override PreValueEditor CreatePreValueEditor()
{
+1 -1
View File
@@ -240,7 +240,7 @@ namespace Umbraco.Web.Routing
/// <remarks>Eg the relative part of <c>/foo/bar/nil</c> to domain <c>example.com/foo</c> is <c>/bar/nil</c>.</remarks>
public static string PathRelativeToDomain(Uri domainUri, string path)
{
return path.Substring(domainUri.AbsolutePath.Length).EnsureStartsWith('/');
return path.Substring(domainUri.GetAbsolutePathDecoded().Length).EnsureStartsWith('/');
}
#endregion
+5 -1
View File
@@ -61,7 +61,11 @@ namespace Umbraco.Web.Routing
GetRolesForLogin = getRolesForLogin ?? (s => Roles.Provider.GetRolesForUser(s));
}
private Func<string, IEnumerable<string>> GetRolesForLogin { get; }
// fixme
// in 7.7 this is cached in the PublishedContentRequest, which ... makes little sense
// killing it entirely, if we need cache, just implement it properly !!
// this is all soooo weird
public Func<string, IEnumerable<string>> GetRolesForLogin { get; }
public PublishedContentRequest CreateRequest(UmbracoContext umbracoContext, Uri uri = null)
{
@@ -150,6 +150,7 @@ namespace Umbraco.Web.Redirects
private static void ContentService_Moving(IContentService sender, MoveEventArgs<IContent> e)
{
//TODO: Use the new e.EventState to track state between Moving/Moved events!
Moving = true;
}
@@ -71,8 +71,8 @@ namespace Umbraco.Web.Scheduling
var results = new HealthCheckResults(checks);
results.LogResults();
// Send using registered notification methods
foreach (var notificationMethod in _notifications)
// Send using registered notification methods that are enabled
foreach (var notificationMethod in _notifications.Where(x => x.Enabled))
await notificationMethod.SendAsync(results, token);
}
@@ -26,6 +26,9 @@ namespace Umbraco.Web.Scheduling
public override async Task<bool> PerformRunAsync(CancellationToken token)
{
if (Suspendable.ScheduledPublishing.CanRun == false)
return true; // repeat, later
switch (_runtime.ServerRole)
{
case ServerRole.Slave:
@@ -120,6 +120,9 @@ namespace Umbraco.Web.Search
static void MemberCacheRefresherUpdated(MemberCacheRefresher sender, CacheRefresherEventArgs args)
{
if (Suspendable.ExamineEvents.CanIndex == false)
return;
switch (args.MessageType)
{
case MessageType.RefreshById:
@@ -162,6 +165,9 @@ namespace Umbraco.Web.Search
static void MediaCacheRefresherUpdated(MediaCacheRefresher sender, CacheRefresherEventArgs args)
{
if (Suspendable.ExamineEvents.CanIndex == false)
return;
if (args.MessageType != MessageType.RefreshByPayload)
throw new NotSupportedException();
@@ -208,6 +214,9 @@ namespace Umbraco.Web.Search
static void ContentCacheRefresherUpdated(ContentCacheRefresher sender, CacheRefresherEventArgs args)
{
if (Suspendable.ExamineEvents.CanIndex == false)
return;
if (args.MessageType != MessageType.RefreshByPayload)
throw new NotSupportedException();
@@ -45,7 +45,8 @@ namespace Umbraco.Web.Security.Identity
services.MemberTypeService,
services.EntityService,
services.ExternalLoginService,
userMembershipProvider));
userMembershipProvider,
UmbracoConfig.For.UmbracoSettings().Content));
app.SetBackOfficeUserManagerType<BackOfficeUserManager, BackOfficeIdentityUser>();
@@ -74,7 +75,8 @@ namespace Umbraco.Web.Security.Identity
(options, owinContext) => BackOfficeUserManager.Create(
options,
customUserStore,
userMembershipProvider));
userMembershipProvider,
UmbracoConfig.For.UmbracoSettings().Content));
app.SetBackOfficeUserManagerType<BackOfficeUserManager, BackOfficeIdentityUser>();
+94 -14
View File
@@ -1,10 +1,12 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.Security;
using LightInject;
using Umbraco.Core;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
@@ -17,6 +19,8 @@ using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.Services;
using Umbraco.Web.Editors;
using Umbraco.Web.Security.Providers;
using Umbraco.Core.Services;
using Umbraco.Web.Routing;
using MPE = global::Umbraco.Core.Security.MembershipProviderExtensions;
namespace Umbraco.Web.Security
@@ -30,26 +34,48 @@ namespace Umbraco.Web.Security
private readonly RoleProvider _roleProvider;
private readonly HttpContextBase _httpContext;
private readonly IPublishedMemberCache _memberCache;
private readonly UmbracoContext _umbracoContext;
// fixme - inject!
private readonly IMemberService _memberService = Current.Services.MemberService;
private readonly IMemberTypeService _memberTypeService = Current.Services.MemberTypeService;
private readonly IUserService _userService = Current.Services.UserService;
private readonly CacheHelper _applicationCache = Current.ApplicationCache;
private readonly ILogger _logger = Current.Logger;
[Inject]
private IMemberService MemberService { get; set; }
[Inject]
private IMemberTypeService MemberTypeService { get; set; }
[Inject]
private IUserService UserService { get; set; }
[Inject]
private IPublicAccessService PublicAccessService { get; set; }
[Inject]
private CacheHelper ApplicationCache { get; set; }
[Inject]
private ILogger Logger { get; set; }
[Inject]
private FacadeRouter Router { get; set; }
#region Constructors
// used here and there for IMember operations (not front-end stuff, no need for _memberCache)
[Obsolete("Use the constructor specifying an UmbracoContext")]
[EditorBrowsable(EditorBrowsableState.Never)]
public MembershipHelper(HttpContextBase httpContext)
{
if (httpContext == null) throw new ArgumentNullException(nameof(httpContext));
_httpContext = httpContext;
_membershipProvider = MPE.GetMembersMembershipProvider();
_roleProvider = Roles.Enabled ? Roles.Provider : new MembersRoleProvider(_memberService);
_roleProvider = Roles.Enabled ? Roles.Provider : new MembersRoleProvider(MemberService);
// _memberCache remains null - not supposed to use it
// alternatively we'd need to get if from the 'current' UmbracoContext?
// helpers are *not* instanciated by the container so we have to
// get our dependencies injected manually, through properties.
Current.Container.InjectProperties(this);
}
// used everywhere
@@ -63,10 +89,16 @@ namespace Umbraco.Web.Security
if (umbracoContext == null) throw new ArgumentNullException(nameof(umbracoContext));
if (membershipProvider == null) throw new ArgumentNullException(nameof(membershipProvider));
if (roleProvider == null) throw new ArgumentNullException(nameof(roleProvider));
_httpContext = umbracoContext.HttpContext;
_umbracoContext = umbracoContext;
_membershipProvider = membershipProvider;
_roleProvider = roleProvider;
_memberCache = umbracoContext.Facade.MemberCache;
// helpers are *not* instanciated by the container so we have to
// get our dependencies injected manually, through properties.
Current.Container.InjectProperties(this);
}
#endregion
@@ -81,6 +113,54 @@ namespace Umbraco.Web.Security
}
}
/// <summary>
/// Check if a document object is protected by the "Protect Pages" functionality in umbraco
/// </summary>
/// <param name="path">The full path of the document object to check</param>
/// <returns>True if the document object is protected</returns>
public virtual bool IsProtected(string path)
{
//this is a cached call
return PublicAccessService.IsProtected(path);
}
/// <summary>
/// Check if the current user has access to a document
/// </summary>
/// <param name="path">The full path of the document object to check</param>
/// <returns>True if the current user has access or if the current document isn't protected</returns>
public virtual bool MemberHasAccess(string path)
{
//cache this in the request cache
return ApplicationCache.RequestCache.GetCacheItem<bool>(string.Format("{0}.{1}-{2}", typeof(MembershipHelper), "MemberHasAccess", path), () =>
{
if (IsProtected(path))
{
return IsLoggedIn() && HasAccess(path, Roles.Provider);
}
return true;
});
}
/// <summary>
/// This will check if the member has access to this path
/// </summary>
/// <param name="path"></param>
/// <param name="roleProvider"></param>
/// <returns></returns>
/// <remarks>
/// This is essentially the same as the PublicAccessServiceExtensions.HasAccess however this will use the PCR cache
/// of the already looked up roles for the member so this doesn't need to happen more than once.
/// This does a safety check in case of things like unit tests where there is no PCR and if that is the case it will use
/// lookup the roles directly.
/// </remarks>
private bool HasAccess(string path, RoleProvider roleProvider)
{
return _umbracoContext.PublishedContentRequest == null
? PublicAccessService.HasAccess(path, CurrentUserName, roleProvider.GetRolesForUser)
: PublicAccessService.HasAccess(path, CurrentUserName, Router.GetRolesForLogin);
}
/// <summary>
/// Returns true if the current membership provider is the Umbraco built-in one.
/// </summary>
@@ -149,7 +229,7 @@ namespace Umbraco.Web.Security
}
}
_memberService.Save(member);
MemberService.Save(member);
//reset the FormsAuth cookie since the username might have changed
FormsAuthentication.SetAuthCookie(member.Username, true);
@@ -184,7 +264,7 @@ namespace Umbraco.Web.Security
if (status != MembershipCreateStatus.Success) return null;
var member = _memberService.GetByUsername(membershipUser.UserName);
var member = MemberService.GetByUsername(membershipUser.UserName);
member.Name = model.Name;
if (model.MemberProperties != null)
@@ -196,7 +276,7 @@ namespace Umbraco.Web.Security
}
}
_memberService.Save(member);
MemberService.Save(member);
}
else
{
@@ -399,7 +479,7 @@ namespace Umbraco.Web.Security
if (provider.IsUmbracoMembershipProvider())
{
memberTypeAlias = memberTypeAlias ?? Constants.Conventions.MemberTypes.DefaultAlias;
var memberType = _memberTypeService.Get(memberTypeAlias);
var memberType = MemberTypeService.Get(memberTypeAlias);
if (memberType == null)
throw new InvalidOperationException("Could not find a member type with alias " + memberTypeAlias);
@@ -644,7 +724,7 @@ namespace Umbraco.Web.Security
/// <returns></returns>
public virtual Attempt<PasswordChangedModel> ChangePassword(string username, ChangingPasswordModel passwordModel, MembershipProvider membershipProvider)
{
var passwordChanger = new PasswordChanger(_logger, _userService);
var passwordChanger = new PasswordChanger(Logger, UserService);
return passwordChanger.ChangePasswordWithMembershipProvider(username, passwordModel, membershipProvider);
}
@@ -709,7 +789,7 @@ namespace Umbraco.Web.Security
/// <returns></returns>
private IMember GetCurrentPersistedMember()
{
return _applicationCache.RequestCache.GetCacheItem<IMember>(
return ApplicationCache.RequestCache.GetCacheItem<IMember>(
GetCacheKey("GetCurrentPersistedMember"), () =>
{
var provider = _membershipProvider;
@@ -719,7 +799,7 @@ namespace Umbraco.Web.Security
throw new NotSupportedException("An IMember model can only be retreived when using the built-in Umbraco membership providers");
}
var username = provider.GetCurrentUserName();
var member = _memberService.GetByUsername(username);
var member = MemberService.GetByUsername(username);
return member;
});
}
@@ -92,6 +92,13 @@ namespace Umbraco.Web.Security.Providers
}
}
protected override Attempt<string> GetRawPassword(string username)
{
var found = MemberService.GetByUsername(username);
if (found == null) return Attempt<string>.Fail();
return Attempt.Succeed(found.RawPasswordValue);
}
public override string DefaultMemberTypeAlias
{
get
@@ -37,10 +37,10 @@ namespace Umbraco.Web.Security.Providers
protected abstract MembershipUser ConvertToMembershipUser(TEntity entity);
private bool _allowManuallyChangingPassword = true;
private bool _allowManuallyChangingPassword = false;
/// <summary>
/// For backwards compatibility, this provider supports this option by default it is true
/// For backwards compatibility, this provider supports this option by default it is false
/// </summary>
public override bool AllowManuallyChangingPassword
{
@@ -66,7 +66,7 @@ namespace Umbraco.Web.Security.Providers
// Initialize base provider class
base.Initialize(name, config);
_allowManuallyChangingPassword = config.GetValue("allowManuallyChangingPassword", true);
_allowManuallyChangingPassword = config.GetValue("allowManuallyChangingPassword", false);
}
/// <summary>
@@ -45,10 +45,36 @@ namespace Umbraco.Web.Security.Providers
return entity.AsConcreteMembershipUser(Name, true);
}
private bool _allowManuallyChangingPassword = false;
private bool _enablePasswordReset = false;
/// <summary>
/// Indicates whether the membership provider is configured to allow users to reset their passwords.
/// </summary>
/// <value></value>
/// <returns>true if the membership provider supports password reset; otherwise, false. The default is FALSE for users.</returns>
public override bool EnablePasswordReset
{
get { return _enablePasswordReset; }
}
/// <summary>
/// For backwards compatibility, this provider supports this option by default it is FALSE for users
/// </summary>
public override bool AllowManuallyChangingPassword
{
get { return _allowManuallyChangingPassword; }
}
public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config)
{
base.Initialize(name, config);
if (config == null) { throw new ArgumentNullException("config"); }
_allowManuallyChangingPassword = config.GetValue("allowManuallyChangingPassword", false);
_enablePasswordReset = config.GetValue("enablePasswordReset", false);
// test for membertype (if not specified, choose the first member type available)
// We'll support both names for legacy reasons: defaultUserTypeAlias & defaultUserGroupAlias
+11 -11
View File
@@ -13,6 +13,7 @@ using Umbraco.Core.Models.Membership;
using Umbraco.Core.Security;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Identity;
using Umbraco.Web.Composing;
using GlobalSettings = Umbraco.Core.Configuration.GlobalSettings;
@@ -48,11 +49,11 @@ namespace Umbraco.Web.Security
IEnumerable<string> allowGroups = null,
IEnumerable<int> allowMembers = null)
{
if (HttpContext.Current == null || Current.RuntimeState.Level != RuntimeLevel.Run)
if (Current.UmbracoContext == null)
{
return false;
}
var helper = new MembershipHelper(new HttpContextWrapper(HttpContext.Current));
var helper = new MembershipHelper(Current.UmbracoContext);
return helper.IsMemberAuthorized(allowAll, allowTypes, allowGroups, allowMembers);
}
@@ -184,8 +185,8 @@ namespace Umbraco.Web.Security
{
var membershipProvider = Core.Security.MembershipProviderExtensions.GetUsersMembershipProvider();
return membershipProvider != null ? membershipProvider.GetUser(username, setOnline) : null;
}
}
/// <summary>
/// Validates the current user to see if they have access to the specified app
/// </summary>
@@ -306,29 +307,28 @@ namespace Umbraco.Web.Security
/// <summary>
/// Checks if the specified user as access to the app
/// </summary>
/// <param name="app"></param>
/// <param name="section"></param>
/// <param name="user"></param>
/// <returns></returns>
internal virtual bool UserHasAppAccess(string app, IUser user)
internal virtual bool UserHasSectionAccess(string section, IUser user)
{
var apps = user.AllowedSections;
return apps.Any(uApp => uApp.InvariantEquals(app));
return user.HasSectionAccess(section);
}
/// <summary>
/// Checks if the specified user by username as access to the app
/// </summary>
/// <param name="app"></param>
/// <param name="section"></param>
/// <param name="username"></param>
/// <returns></returns>
internal bool UserHasAppAccess(string app, string username)
internal bool UserHasSectionAccess(string section, string username)
{
var user = _userService.GetByUsername(username);
if (user == null)
{
return false;
}
return UserHasAppAccess(app, user);
return user.HasSectionAccess(section);
}
[Obsolete("Returns the current user's unique umbraco sesion id - this cannot be set and isn't intended to be used in your code")]
+106
View File
@@ -0,0 +1,106 @@
using System;
using Examine;
using Examine.Providers;
using Umbraco.Core.Composing;
using Umbraco.Web.Cache;
namespace Umbraco.Web
{
internal static class Suspendable
{
public static class PageCacheRefresher
{
private static bool _tried, _suspended;
public static bool CanRefreshDocumentCacheFromDatabase
{
get
{
// trying a full refresh
if (_suspended == false) return true;
_tried = true; // remember we tried
return false;
}
}
// trying a partial update
// ok if not suspended, or if we haven't done a full already
public static bool CanUpdateDocumentCache => _suspended == false || _tried == false;
public static void SuspendDocumentCache()
{
Current.ProfilingLogger.Logger.Info(typeof (PageCacheRefresher), "Suspend document cache.");
_suspended = true;
}
public static void ResumeDocumentCache()
{
_suspended = false;
Current.ProfilingLogger.Logger.Info(typeof (PageCacheRefresher), $"Resume document cache (reload:{(_tried ? "true" : "false")}).");
if (_tried == false) return;
_tried = false;
var pageRefresher = Current.CacheRefreshers[ContentCacheRefresher.UniqueId];
pageRefresher.RefreshAll();
}
}
public static class ExamineEvents
{
private static bool _tried, _suspended;
public static bool CanIndex
{
get
{
if (_suspended == false) return true;
_tried = true; // remember we tried
return false;
}
}
public static void SuspendIndexers()
{
Current.ProfilingLogger.Logger.Info(typeof (ExamineEvents), "Suspend indexers.");
_suspended = true;
}
public static void ResumeIndexers()
{
_suspended = false;
Current.ProfilingLogger.Logger.Info(typeof (ExamineEvents), $"Resume indexers (rebuild:{(_tried ? "true" : "false")}).");
if (_tried == false) return;
_tried = false;
// fixme - could we fork this on a background thread?
foreach (BaseIndexProvider indexer in ExamineManager.Instance.IndexProviderCollection)
{
indexer.RebuildIndex();
}
}
}
public static class ScheduledPublishing
{
private static bool _suspended;
public static bool CanRun => _suspended == false;
public static void Suspend()
{
Current.ProfilingLogger.Logger.Info(typeof (ScheduledPublishing), "Suspend scheduled publishing.");
_suspended = true;
}
public static void Resume()
{
Current.ProfilingLogger.Logger.Info(typeof (ScheduledPublishing), "Resume scheduled publishing.");
_suspended = false;
}
}
}
}
@@ -7,7 +7,6 @@ using System.Threading.Tasks;
using System.Web.Http;
using Umbraco.Core.Models;
using Umbraco.Core.Services;
using Umbraco.Web.Composing;
using Umbraco.Web.Models.Trees;
using Umbraco.Web.Mvc;
using Umbraco.Web.WebApi;
@@ -36,7 +35,7 @@ namespace Umbraco.Web.Trees
var rootId = Constants.System.Root.ToString(CultureInfo.InvariantCulture);
//find all tree definitions that have the current application alias
var appTrees = Current.Services.ApplicationTreeService.GetApplicationTrees(application, onlyInitialized).ToArray();
var appTrees = Services.ApplicationTreeService.GetApplicationTrees(application, onlyInitialized).ToArray();
if (appTrees.Length == 1 || string.IsNullOrEmpty(tree) == false )
{
@@ -122,8 +121,8 @@ namespace Umbraco.Web.Trees
//if the root node has a route path, we cannot create a single root section because by specifying the route path this would
//override the dashboard route and that means there can be no dashboard for that section which is a breaking change.
if (string.IsNullOrWhiteSpace(rootNode.Result.RoutePath) == false
&& rootNode.Result.RoutePath != "#"
if (string.IsNullOrWhiteSpace(rootNode.Result.RoutePath) == false
&& rootNode.Result.RoutePath != "#"
&& rootNode.Result.RoutePath != application)
{
//null indicates this cannot be converted
@@ -90,8 +90,9 @@ namespace Umbraco.Web.Trees
var menu = new MenuItemCollection();
if (id == Constants.System.Root.ToInvariantString())
{
// root actions
{
// root actions
menu.Items.Add<ActionNew>(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias)));
menu.Items.Add<RefreshNode, ActionRefresh>(Services.TextService.Localize(string.Format("actions/{0}", ActionRefresh.Instance.Alias)), true);
return menu;
}
+16 -23
View File
@@ -35,20 +35,7 @@ namespace Umbraco.Web.Trees
public class ContentTreeController : ContentTreeControllerBase, ISearchableTree
{
private readonly UmbracoTreeSearcher _treeSearcher = new UmbracoTreeSearcher();
protected override TreeNode CreateRootNode(FormDataCollection queryStrings)
{
var node = base.CreateRootNode(queryStrings);
// if the user's start node is not default, then ensure the root doesn't have a menu
if (UserStartNodes.Contains(Constants.System.Root) == false)
{
node.MenuUrl = "";
}
node.Name = Services.TextService.Localize("sections/"+ Constants.Trees.Content);
return node;
}
protected override int RecycleBinId => Constants.System.RecycleBinContent;
protected override bool RecycleBinSmells => Services.ContentService.RecycleBinSmells();
@@ -112,9 +99,12 @@ namespace Umbraco.Web.Trees
{
var menu = new MenuItemCollection();
// if the user's start node is not the root then ensure the root menu is empty/doesn't exist
// if the user's start node is not the root then the only menu item to display is refresh
if (UserStartNodes.Contains(Constants.System.Root) == false)
{
menu.Items.Add<RefreshNode, ActionRefresh>(
Services.TextService.Localize(string.Concat("actions/", ActionRefresh.Instance.Alias)),
true);
return menu;
}
@@ -158,6 +148,16 @@ namespace Umbraco.Web.Trees
throw new HttpResponseException(HttpStatusCode.NotFound);
}
//if the user has no path access for this node, all they can do is refresh
if (Security.CurrentUser.HasPathAccess(item, Services.EntityService, RecycleBinId) == false)
{
var menu = new MenuItemCollection();
menu.Items.Add<RefreshNode, ActionRefresh>(
Services.TextService.Localize(string.Concat("actions/", ActionRefresh.Instance.Alias)),
true);
return menu;
}
var nodeMenu = GetAllNodeMenuItems(item);
var allowedMenuItems = GetAllowedUserMenuItemsForNode(item);
@@ -193,14 +193,7 @@ namespace Umbraco.Web.Trees
protected override bool HasPathAccess(string id, FormDataCollection queryStrings)
{
var entity = GetEntityFromId(id);
if (entity == null)
return false;
var content = Services.ContentService.GetById(entity.Id);
if (content == null)
return false;
return Security.CurrentUser.HasPathAccess(content, Services.EntityService);
return HasPathAccess(entity, queryStrings);
}
/// <summary>
@@ -1,5 +1,7 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Net;
using System.Net.Http;
@@ -52,8 +54,48 @@ namespace Umbraco.Web.Trees
#endregion
/// <summary>
/// Ensure the noAccess metadata is applied for the root node if in dialog mode and the user doesn't have path access to it
/// </summary>
/// <param name="queryStrings"></param>
/// <returns></returns>
protected override TreeNode CreateRootNode(FormDataCollection queryStrings)
{
var node = base.CreateRootNode(queryStrings);
if (IsDialog(queryStrings) && UserStartNodes.Contains(Constants.System.Root) == false)
{
node.AdditionalData["noAccess"] = true;
}
return node;
}
protected abstract TreeNode GetSingleTreeNode(IUmbracoEntity e, string parentId, FormDataCollection queryStrings);
/// <summary>
/// Returns a <see cref="TreeNode"/> for the <see cref="IUmbracoEntity"/> and
/// attaches some meta data to the node if the user doesn't have start node access to it when in dialog mode
/// </summary>
/// <param name="e"></param>
/// <param name="parentId"></param>
/// <param name="queryStrings"></param>
/// <returns></returns>
internal TreeNode GetSingleTreeNodeWithAccessCheck(IUmbracoEntity e, string parentId, FormDataCollection queryStrings)
{
bool hasPathAccess;
var entityIsAncestorOfStartNodes = Security.CurrentUser.IsInBranchOfStartNode(e, Services.EntityService, RecycleBinId, out hasPathAccess);
if (entityIsAncestorOfStartNodes == false)
return null;
var treeNode = GetSingleTreeNode(e, parentId, queryStrings);
if (hasPathAccess == false)
{
treeNode.AdditionalData["noAccess"] = true;
}
return treeNode;
}
/// <summary>
/// Returns the
/// </summary>
@@ -68,13 +110,7 @@ namespace Umbraco.Web.Trees
/// Returns the user's start node for this tree
/// </summary>
protected abstract int[] UserStartNodes { get; }
/// <summary>
/// Gets the tree nodes for the given id
/// </summary>
/// <param name="id"></param>
/// <param name="queryStrings"></param>
/// <returns></returns>
protected virtual TreeNodeCollection PerformGetTreeNodes(string id, FormDataCollection queryStrings)
{
var nodes = new TreeNodeCollection();
@@ -82,9 +118,10 @@ namespace Umbraco.Web.Trees
var altStartId = string.Empty;
if (queryStrings.HasKey(TreeQueryStringParameters.StartNodeId))
altStartId = queryStrings.GetValue<string>(TreeQueryStringParameters.StartNodeId);
var rootIdString = Constants.System.Root.ToString(CultureInfo.InvariantCulture);
//check if a request has been made to render from a specific start node
if (string.IsNullOrEmpty(altStartId) == false && altStartId != "undefined" && altStartId != Constants.System.Root.ToString(CultureInfo.InvariantCulture))
if (string.IsNullOrEmpty(altStartId) == false && altStartId != "undefined" && altStartId != rootIdString)
{
id = altStartId;
@@ -110,10 +147,42 @@ namespace Umbraco.Web.Trees
}
}
var entities = GetChildEntities(id);
nodes.AddRange(entities.Select(entity => GetSingleTreeNode(entity, id, queryStrings)).Where(node => node != null));
var entities = GetChildEntities(id).ToList();
//If we are looking up the root and there is more than one node ...
//then we want to lookup those nodes' 'site' nodes and render those so that the
//user has some context of where they are in the tree, this is generally for pickers in a dialog.
//for any node they don't have access too, we need to add some metadata
if (id == rootIdString && entities.Count > 1)
{
var siteNodeIds = new List<int>();
//put into array since we might modify the list
foreach (var e in entities.ToArray())
{
var pathParts = e.Path.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries);
if (pathParts.Length < 2)
continue; // this should never happen but better to check
int siteNodeId;
if (int.TryParse(pathParts[1], out siteNodeId) == false)
continue;
//we'll look up this
siteNodeIds.Add(siteNodeId);
}
var siteNodes = Services.EntityService.GetAll(UmbracoObjectType, siteNodeIds.ToArray())
.DistinctBy(e => e.Id)
.ToArray();
//add site nodes
nodes.AddRange(siteNodes.Select(e => GetSingleTreeNodeWithAccessCheck(e, id, queryStrings)).Where(node => node != null));
return nodes;
}
nodes.AddRange(entities.Select(e => GetSingleTreeNodeWithAccessCheck(e, id, queryStrings)).Where(node => node != null));
return nodes;
}
}
protected abstract MenuItemCollection PerformGetMenuForNode(string id, FormDataCollection queryStrings);
@@ -150,8 +219,21 @@ namespace Umbraco.Web.Trees
/// <param name="id"></param>
/// <param name="queryStrings"></param>
/// <returns></returns>
//we should remove this in v8, it's now here for backwards compat only
protected abstract bool HasPathAccess(string id, FormDataCollection queryStrings);
/// <summary>
/// Returns true or false if the current user has access to the node based on the user's allowed start node (path) access
/// </summary>
/// <param name="entity"></param>
/// <param name="queryStrings"></param>
/// <returns></returns>
protected bool HasPathAccess(IUmbracoEntity entity, FormDataCollection queryStrings)
{
if (entity == null) return false;
return Security.CurrentUser.HasPathAccess(entity, Services.EntityService, RecycleBinId);
}
/// <summary>
/// Ensures the recycle bin is appended when required (i.e. user has access to the root and it's not in dialog mode)
/// </summary>
@@ -210,7 +292,7 @@ namespace Umbraco.Web.Trees
/// </remarks>
private TreeNodeCollection GetTreeNodesInternal(string id, FormDataCollection queryStrings)
{
IUmbracoEntity current = GetEntityFromId(id);
var current = GetEntityFromId(id);
//before we get the children we need to see if this is a container node
@@ -239,6 +321,7 @@ namespace Umbraco.Web.Trees
menu.Items.Add<ActionRefresh>(Services.TextService.Localize("actions", ActionRefresh.Instance.Alias), true);
return menu;
}
return PerformGetMenuForNode(id, queryStrings);
}
@@ -268,10 +351,11 @@ namespace Umbraco.Web.Trees
internal IEnumerable<MenuItem> GetAllowedUserMenuItemsForNode(IUmbracoEntity dd)
{
var permission = Services.UserService.GetPermissions(Security.CurrentUser, dd.Path);
var actions = global::Umbraco.Web._Legacy.Actions.Action.FromEntityPermission(permission);
var actions = global::Umbraco.Web._Legacy.Actions.Action.FromEntityPermission(permission)
.ToList();
// A user is allowed to delete their own stuff
if (dd.CreatorId == Security.CurrentUser.Id && actions.Contains(ActionDelete.Instance) == false)
if (dd.CreatorId == Security.GetUserId() && actions.Contains(ActionDelete.Instance) == false)
actions.Add(ActionDelete.Instance);
return actions.Select(x => new MenuItem(x));
@@ -290,34 +374,71 @@ namespace Umbraco.Web.Trees
}
/// <summary>
/// this will parse the string into either a GUID or INT
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
internal Tuple<Guid?, int?> GetIdentifierFromString(string id)
{
Guid idGuid;
int idInt;
Udi idUdi;
if (Guid.TryParse(id, out idGuid))
{
return new Tuple<Guid?, int?>(idGuid, null);
}
if (int.TryParse(id, out idInt))
{
return new Tuple<Guid?, int?>(null, idInt);
}
if (Udi.TryParse(id, out idUdi))
{
var guidUdi = idUdi as GuidUdi;
if (guidUdi != null)
return new Tuple<Guid?, int?>(guidUdi.Guid, null);
}
return null;
}
/// <summary>
/// Get an entity via an id that can be either an integer, Guid or UDI
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
/// <remarks>
/// This object has it's own contextual cache for these lookups
/// </remarks>
internal IUmbracoEntity GetEntityFromId(string id)
{
IUmbracoEntity entity;
return _entityCache.GetOrAdd(id, s =>
{
IUmbracoEntity entity;
if (Guid.TryParse(id, out Guid idGuid))
{
entity = Services.EntityService.GetByKey(idGuid, UmbracoObjectType);
}
else if (int.TryParse(id, out int idInt))
{
entity = Services.EntityService.Get(idInt, UmbracoObjectType);
}
else if (Udi.TryParse(id, out Udi idUdi))
{
var guidUdi = idUdi as GuidUdi;
entity = guidUdi != null ? Services.EntityService.GetByKey(guidUdi.Guid, UmbracoObjectType) : null;
}
else
{
return null;
}
if (Guid.TryParse(s, out Guid idGuid))
{
entity = Services.EntityService.GetByKey(idGuid, UmbracoObjectType);
}
else if (int.TryParse(s, out int idInt))
{
entity = Services.EntityService.Get(idInt, UmbracoObjectType);
}
else if (Udi.TryParse(s, out Udi idUdi))
{
var guidUdi = idUdi as GuidUdi;
entity = guidUdi != null ? Services.EntityService.GetByKey(guidUdi.Guid, UmbracoObjectType) : null;
}
else
{
return null;
}
return entity;
return entity;
});
}
private readonly ConcurrentDictionary<string, IUmbracoEntity> _entityCache = new ConcurrentDictionary<string, IUmbracoEntity>();
}
}

Some files were not shown because too many files have changed in this diff Show More