From 9ed65769080ede82d2cd703032af2da11e1adb3e Mon Sep 17 00:00:00 2001 From: Stephan Date: Tue, 19 Sep 2017 15:51:47 +0200 Subject: [PATCH] Port 7.7 - WIP --- .../ClientDependencyConfiguration.cs | 4 +- src/Umbraco.Core/Models/Membership/User.cs | 2 +- .../Repositories/DictionaryRepository.cs | 2 +- .../Interfaces/IMediaRepository.cs | 21 +- .../Repositories/UserRepository.cs | 2 +- .../Persistence/UmbracoDatabase.cs | 2 +- .../Security/BackOfficeUserManager.cs | 15 +- .../Security/MembershipProviderBase.cs | 1 + .../Services/LocalizationService.cs | 4 +- src/Umbraco.Core/Services/MediaService.cs | 10 +- .../Services/PublicAccessService.cs | 30 +- src/Umbraco.Core/UdiEntityType.cs | 1 - src/Umbraco.Core/Umbraco.Core.csproj | 4 + .../Cache/CacheRefresherTests.cs | 8 +- src/Umbraco.Tests/CoreThings/UdiTests.cs | 151 ++++++- .../FrontEnd/UmbracoHelperTests.cs | 89 ++++ .../{ => Misc}/DateTimeExtensionsTests.cs | 2 +- src/Umbraco.Tests/Misc/HashGeneratorTests.cs | 184 ++++++++ .../Models/UserExtensionsTests.cs | 3 + .../Repositories/ContentRepositoryTest.cs | 83 +++- .../Repositories/DictionaryRepositoryTest.cs | 22 +- .../Repositories/MediaRepositoryTest.cs | 71 ++- .../Repositories/SimilarNodeNameTests.cs | 94 ++++ .../Persistence/SqlCeTableByTableTest.cs | 1 + .../Plugins/PluginManagerTests.cs | 10 +- .../Routing/ContentFinderByNiceUrlTests.cs | 79 +++- .../Services/ContentServiceTests.cs | 57 +++ .../Services/EntityServiceTests.cs | 12 +- .../Services/MemberServiceTests.cs | 50 ++ .../Services/ThreadSafetyServiceTest.cs | 1 + .../Services/UserServiceTests.cs | 44 +- .../Strings/StringExtensionsTests.cs | 2 +- src/Umbraco.Tests/TestHelpers/BaseWebTest.cs | 2 + .../TestControllerActivatorBase.cs | 16 +- .../TestHelpers/Entities/MockedMember.cs | 23 + src/Umbraco.Tests/TestHelpers/TestObjects.cs | 4 +- src/Umbraco.Tests/Umbraco.Tests.csproj | 7 +- .../UserEditorAuthorizationHelperTests.cs | 427 ++++++++++++++++++ .../Web/Controllers/UsersControllerTests.cs | 5 +- .../Web/HttpCookieExtensionsTests.cs | 44 ++ .../Web/Mvc/HtmlStringUtilitiesTests.cs | 26 ++ .../Cache/ContentCacheRefresher.cs | 8 +- .../Cache/TemplateCacheRefresher.cs | 2 +- .../Editors/BackOfficeController.cs | 2 +- .../Editors/BackOfficeServerVariables.cs | 3 +- src/Umbraco.Web/Editors/ContentController.cs | 10 +- .../Editors/ContentPostValidateAttribute.cs | 9 +- .../Editors/ContentTypeController.cs | 15 +- .../Editors/CurrentUserController.cs | 7 +- .../Editors/DashboardController.cs | 50 +- src/Umbraco.Web/Editors/DashboardHelper.cs | 96 ++++ src/Umbraco.Web/Editors/DataTypeController.cs | 15 +- .../Editors/MediaTypeController.cs | 10 + src/Umbraco.Web/Editors/PasswordChanger.cs | 51 ++- src/Umbraco.Web/Editors/SectionController.cs | 51 ++- .../Editors/UserEditorAuthorizationHelper.cs | 155 +++++++ .../UserGroupAuthorizationAttribute.cs | 62 +++ .../UserGroupEditorAuthorizationHelper.cs | 122 +++++ .../Editors/UserGroupsController.cs | 53 ++- src/Umbraco.Web/Editors/UsersController.cs | 199 ++++++-- .../Checks/Config/CustomErrorsCheck.cs | 3 +- .../FolderAndFilePermissionsCheck.cs | 31 +- .../EmailNotificationMethod.cs | 11 +- .../IHealthCheckNotificationMethod.cs | 1 + src/Umbraco.Web/HtmlStringUtilities.cs | 74 ++- src/Umbraco.Web/HttpCookieExtensions.cs | 34 ++ .../Install/Controllers/InstallController.cs | 2 + .../Install/FilePermissionHelper.cs | 74 ++- .../Models/ChangingPasswordModel.cs | 15 +- .../ContentEditing/ContentItemDisplay.cs | 12 +- .../Models/ContentEditing/Section.cs | 15 +- .../Models/ContentEditing/UserDetail.cs | 8 +- .../Models/ContentEditing/UserDisplay.cs | 39 +- .../Models/ContentEditing/UserInvite.cs | 12 +- src/Umbraco.Web/Models/LoginStatusModel.cs | 5 +- .../Models/Mapping/ContentProfile.cs | 11 +- .../Models/Mapping/SectionProfile.cs | 5 +- src/Umbraco.Web/Models/Mapping/UserProfile.cs | 82 ++-- src/Umbraco.Web/Models/ProfileModel.cs | 5 +- src/Umbraco.Web/Models/RegisterModel.cs | 5 +- .../PropertyEditors/TextAreaPropertyEditor.cs | 8 +- .../PropertyEditors/TextOnlyValueEditor.cs | 46 ++ .../PropertyEditors/TextboxPropertyEditor.cs | 13 +- src/Umbraco.Web/Routing/DomainHelper.cs | 2 +- src/Umbraco.Web/Routing/FacadeRouter.cs | 6 +- .../Routing/RedirectTrackingComponent.cs | 1 + .../Scheduling/HealthCheckNotifier.cs | 4 +- .../Scheduling/ScheduledPublishing.cs | 3 + src/Umbraco.Web/Search/ExamineComponent.cs | 9 + .../Security/Identity/AppBuilderExtensions.cs | 6 +- src/Umbraco.Web/Security/MembershipHelper.cs | 108 ++++- .../Providers/MembersMembershipProvider.cs | 7 + .../Providers/UmbracoMembershipProvider.cs | 6 +- .../Providers/UsersMembershipProvider.cs | 26 ++ src/Umbraco.Web/Security/WebSecurity.cs | 22 +- src/Umbraco.Web/Suspendable.cs | 106 +++++ .../Trees/ApplicationTreeController.cs | 7 +- .../Trees/ContentBlueprintTreeController.cs | 5 +- .../Trees/ContentTreeController.cs | 39 +- .../Trees/ContentTreeControllerBase.cs | 189 ++++++-- .../Trees/ContentTypeTreeController.cs | 7 + .../Trees/DataTypeTreeController.cs | 5 + src/Umbraco.Web/Trees/LegacyTreeParams.cs | 17 +- src/Umbraco.Web/Trees/MediaTreeController.cs | 38 +- .../Trees/MediaTypeTreeController.cs | 5 + .../Trees/TemplatesTreeController.cs | 5 +- src/Umbraco.Web/Trees/UserTreeController.cs | 2 +- .../UI/Pages/UmbracoEnsuredPage.cs | 44 +- src/Umbraco.Web/Umbraco.Web.csproj | 9 + src/Umbraco.Web/UmbracoDefaultOwinStartup.cs | 19 +- src/Umbraco.Web/UmbracoHelper.cs | 72 ++- .../WebApi/EnableDetailedErrorsAttribute.cs | 17 + .../Filters/AngularAntiForgeryHelper.cs | 20 +- .../Filters/LegacyTreeAuthorizeAttribute.cs | 2 +- .../UmbracoApplicationAuthorizeAttribute.cs | 2 +- .../Filters/UmbracoTreeAuthorizeAttribute.cs | 2 +- .../WebApi/UmbracoAuthorizedApiController.cs | 4 +- ...edExceptionLoggerConfigurationAttribute.cs | 29 ++ .../WebApi/UnhandledExceptionLogger.cs | 37 ++ .../UmbracoAuthorizedHttpHandler.cs | 4 +- .../UmbracoAuthorizedWebService.cs | 4 +- .../_Legacy/UI/LegacyDialogHandler.cs | 42 +- .../umbraco.presentation/library.cs | 4 +- .../umbraco/dialogs/AssignDomain2.aspx.cs | 21 +- .../umbraco/dialogs/sendToTranslation.aspx.cs | 1 + .../umbraco/dialogs/sort.aspx.cs | 39 +- 126 files changed, 3447 insertions(+), 596 deletions(-) rename src/Umbraco.Tests/{ => Misc}/DateTimeExtensionsTests.cs (98%) create mode 100644 src/Umbraco.Tests/Misc/HashGeneratorTests.cs create mode 100644 src/Umbraco.Tests/Persistence/Repositories/SimilarNodeNameTests.cs create mode 100644 src/Umbraco.Tests/Web/Controllers/UserEditorAuthorizationHelperTests.cs create mode 100644 src/Umbraco.Tests/Web/HttpCookieExtensionsTests.cs create mode 100644 src/Umbraco.Tests/Web/Mvc/HtmlStringUtilitiesTests.cs create mode 100644 src/Umbraco.Web/Editors/DashboardHelper.cs create mode 100644 src/Umbraco.Web/Editors/UserEditorAuthorizationHelper.cs create mode 100644 src/Umbraco.Web/Editors/UserGroupAuthorizationAttribute.cs create mode 100644 src/Umbraco.Web/Editors/UserGroupEditorAuthorizationHelper.cs create mode 100644 src/Umbraco.Web/PropertyEditors/TextOnlyValueEditor.cs create mode 100644 src/Umbraco.Web/Suspendable.cs create mode 100644 src/Umbraco.Web/WebApi/EnableDetailedErrorsAttribute.cs create mode 100644 src/Umbraco.Web/WebApi/UnhandedExceptionLoggerConfigurationAttribute.cs create mode 100644 src/Umbraco.Web/WebApi/UnhandledExceptionLogger.cs diff --git a/src/Umbraco.Core/Configuration/ClientDependencyConfiguration.cs b/src/Umbraco.Core/Configuration/ClientDependencyConfiguration.cs index 0add427ee3..01fdbd17a4 100644 --- a/src/Umbraco.Core/Configuration/ClientDependencyConfiguration.cs +++ b/src/Umbraco.Core/Configuration/ClientDependencyConfiguration.cs @@ -82,7 +82,7 @@ namespace Umbraco.Core.Configuration catch (Exception ex) { //invalid path format or something... try/catch to be safe - LogHelper.Error("Could not get path from ClientDependency.config", ex); + _logger.Error("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("Could not clear temp files", ex); + _logger.Error("Could not clear temp files", ex); success = false; } } diff --git a/src/Umbraco.Core/Models/Membership/User.cs b/src/Umbraco.Core/Models/Membership/User.cs index 00ef115afd..b3f13629cd 100644 --- a/src/Umbraco.Core/Models/Membership/User.cs +++ b/src/Umbraco.Core/Models/Membership/User.cs @@ -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); diff --git a/src/Umbraco.Core/Persistence/Repositories/DictionaryRepository.cs b/src/Umbraco.Core/Persistence/Repositories/DictionaryRepository.cs index a0d9d14359..6fa3649d2e 100644 --- a/src/Umbraco.Core/Persistence/Repositories/DictionaryRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/DictionaryRepository.cs @@ -252,7 +252,7 @@ namespace Umbraco.Core.Persistence.Repositories public Dictionary GetDictionaryItemKeyMap() { var columns = new[] { "key", "id" }.Select(x => (object) SqlSyntax.GetQuotedColumnName(x)).ToArray(); - var sql = new Sql().Select(columns).From(SqlSyntax); + var sql = UnitOfWork.Sql().Select(columns).From(); return Database.Fetch(sql).ToDictionary(x => x.Key, x => x.Id); } diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs index 06b812d16e..8d9d3ec582 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs @@ -1,29 +1,10 @@ using System; -using System.Xml.Linq; using Umbraco.Core.Models; namespace Umbraco.Core.Persistence.Repositories { public interface IMediaRepository : IRepositoryVersionable, IRecycleBinRepository, IReadRepository { - /// - /// Used to add/update published xml for the media item - /// - /// - /// - void AddOrUpdateContentXml(IMedia content, Func xml); - - /// - /// Used to remove the content xml for a content item - /// - /// - void DeleteContentXml(IMedia content); - - /// - /// Used to add/update preview xml for the content item - /// - /// - /// - void AddOrUpdatePreviewXml(IMedia content, Func xml); + IMedia GetMediaByPath(string mediaPath); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs index ec15b22c7b..97ea564129 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs @@ -600,7 +600,7 @@ ORDER BY colName"; /// public IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, Expression> orderBy, Direction orderDirection = Direction.Ascending, - string[] userGroups = null, UserState[] userState = null, IQuery filter = null) + string[] includeUserGroups = null, string[] excludeUserGroups = null, UserState[] userState = null, IQuery filter = null) { if (orderBy == null) throw new ArgumentNullException(nameof(orderBy)); diff --git a/src/Umbraco.Core/Persistence/UmbracoDatabase.cs b/src/Umbraco.Core/Persistence/UmbracoDatabase.cs index 43735b7759..f5a87a8f72 100644 --- a/src/Umbraco.Core/Persistence/UmbracoDatabase.cs +++ b/src/Umbraco.Core/Persistence/UmbracoDatabase.cs @@ -129,7 +129,7 @@ namespace Umbraco.Core.Persistence /// internal bool EnableSqlCount { - get { return _enableCount; } + get => _enableCount; set { _enableCount = value; diff --git a/src/Umbraco.Core/Security/BackOfficeUserManager.cs b/src/Umbraco.Core/Security/BackOfficeUserManager.cs index 3593afe18e..1f12500f52 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserManager.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserManager.cs @@ -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 options, - IUserService userService, - IExternalLoginService externalLoginService, - MembershipProviderBase membershipProvider) - { - return Create(options, userService, - ApplicationContext.Current.Services.EntityService, - externalLoginService, membershipProvider, - UmbracoConfig.For.UmbracoSettings().Content); - } - /// /// Creates a BackOfficeUserManager instance with all default options and the default BackOfficeUserManager /// diff --git a/src/Umbraco.Core/Security/MembershipProviderBase.cs b/src/Umbraco.Core/Security/MembershipProviderBase.cs index 1c0f407429..79eaa44afd 100644 --- a/src/Umbraco.Core/Security/MembershipProviderBase.cs +++ b/src/Umbraco.Core/Security/MembershipProviderBase.cs @@ -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"))) diff --git a/src/Umbraco.Core/Services/LocalizationService.cs b/src/Umbraco.Core/Services/LocalizationService.cs index 2d0d24e4b0..e7e836db0a 100644 --- a/src/Umbraco.Core/Services/LocalizationService.cs +++ b/src/Umbraco.Core/Services/LocalizationService.cs @@ -411,9 +411,9 @@ namespace Umbraco.Core.Services public Dictionary GetDictionaryItemKeyMap() { - using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { - var repository = RepositoryFactory.CreateDictionaryRepository(uow); + var repository = uow.CreateRepository(); return repository.GetDictionaryItemKeyMap(); } } diff --git a/src/Umbraco.Core/Services/MediaService.cs b/src/Umbraco.Core/Services/MediaService.cs index 64234a66f4..81125063e4 100644 --- a/src/Umbraco.Core/Services/MediaService.cs +++ b/src/Umbraco.Core/Services/MediaService.cs @@ -111,7 +111,7 @@ namespace Umbraco.Core.Services var parent = GetById(parentId); return CreateMedia(name, parent, mediaTypeAlias, userId); } - + /// /// Creates an object of a specified media type. /// @@ -378,7 +378,7 @@ namespace Umbraco.Core.Services return repository.GetAll(idsA); } } - + /// /// Gets a collection of objects by the Id of the /// @@ -632,7 +632,7 @@ namespace Umbraco.Core.Services var query = uow.Query(); //if the id is System Root, then just get all if (id != Constants.System.Root) - { + { var entityRepository = uow.CreateRepository(); 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(media, evtMsgs); + var saveEventArgs = new SaveEventArgs(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(); diff --git a/src/Umbraco.Core/Services/PublicAccessService.cs b/src/Umbraco.Core/Services/PublicAccessService.cs index edba498694..1b7f3e1e18 100644 --- a/src/Umbraco.Core/Services/PublicAccessService.cs +++ b/src/Umbraco.Core/Services/PublicAccessService.cs @@ -132,17 +132,18 @@ namespace Umbraco.Core.Services var saveEventArgs = new SaveEventArgs(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(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(entry, evtMsgs); if (uow.Events.DispatchCancelable(Saving, this, saveEventArgs)) { - uow.Commit(); + uow.Complete(); return OperationStatus.Attempt.Cancel(evtMsgs); } var repo = uow.CreateRepository(); 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(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(); 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); } diff --git a/src/Umbraco.Core/UdiEntityType.cs b/src/Umbraco.Core/UdiEntityType.cs index 50d2b4e46b..047bb3198f 100644 --- a/src/Umbraco.Core/UdiEntityType.cs +++ b/src/Umbraco.Core/UdiEntityType.cs @@ -55,7 +55,6 @@ namespace Umbraco.Core { PartialView, UdiType.StringUdi}, { PartialViewMacro, UdiType.StringUdi}, { Stylesheet, UdiType.StringUdi}, - { UserControl, UdiType.StringUdi}, { Xslt, UdiType.StringUdi}, }; } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 2c53376e2e..e22b06b565 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -54,6 +54,9 @@ + + 1.9.2 + @@ -1311,6 +1314,7 @@ + diff --git a/src/Umbraco.Tests/Cache/CacheRefresherTests.cs b/src/Umbraco.Tests/Cache/CacheRefresherTests.cs index bd8ccdacbd..8002cb9b4f 100644 --- a/src/Umbraco.Tests/Cache/CacheRefresherTests.cs +++ b/src/Umbraco.Tests/Cache/CacheRefresherTests.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) diff --git a/src/Umbraco.Tests/CoreThings/UdiTests.cs b/src/Umbraco.Tests/CoreThings/UdiTests.cs index 349fea22e9..67e2373ffe 100644 --- a/src/Umbraco.Tests/CoreThings/UdiTests.cs +++ b/src/Umbraco.Tests/CoreThings/UdiTests.cs @@ -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(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(() => Udi.Create(Constants.UdiEntityType.AnyString, guid)); - Assert.Throws(() => Udi.Create(Constants.UdiEntityType.AnyGuid, "foo")); - Assert.Throws(() => 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(udi); + + udi = Udi.Parse("umb://any-guid/00000000000000000000000000000000"); + Assert.IsTrue(udi.IsRoot); + Assert.IsInstanceOf(udi); + + udi = Udi.Parse("umb://any-guid/"); + Assert.IsTrue(udi.IsRoot); + Assert.IsInstanceOf(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(udi); + + // known + Assert.IsTrue(Udi.TryParse("umb://foo/A87F65C8D6B94E868F6949BA92C93045", true, out udi)); + Assert.IsInstanceOf(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 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 differences = null) + { + throw new NotImplementedException(); + } + } } } diff --git a/src/Umbraco.Tests/FrontEnd/UmbracoHelperTests.cs b/src/Umbraco.Tests/FrontEnd/UmbracoHelperTests.cs index 1c8b943575..60adf49bb5 100644 --- a/src/Umbraco.Tests/FrontEnd/UmbracoHelperTests.cs +++ b/src/Umbraco.Tests/FrontEnd/UmbracoHelperTests.cs @@ -21,6 +21,21 @@ namespace Umbraco.Tests.FrontEnd Assert.AreEqual("Hello world, this is some…", result); } + /// + /// If a truncated string ends with a space, we should trim the space before appending the ellipsis. + /// + [Test] + public void Truncate_Simple_With_Trimming() + { + var text = "Hello world, this is some text with a link"; + + var helper = new UmbracoHelper(); + + var result = helper.Truncate(text, 26).ToString(); + + Assert.AreEqual("Hello world, this is some…", 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 with a link"; + + var helper = new UmbracoHelper(); + + var result = helper.TruncateByWords(text, 4).ToString(); + + Assert.AreEqual("Hello world, this is…", result); + } + + [Test] + public void Truncate_By_Words_With_Tag() + { + var text = "Hello world, this is some text with a link"; + + var helper = new UmbracoHelper(); + + var result = helper.TruncateByWords(text, 4).ToString(); + + Assert.AreEqual("Hello world, this is…", result); + } + + [Test] + public void Truncate_By_Words_Mid_Tag() + { + var text = "Hello world, this is some text with a link"; + + var helper = new UmbracoHelper(); + + var result = helper.TruncateByWords(text, 7).ToString(); + + Assert.AreEqual("Hello world, this is some text with…", result); + } + + [Test] + public void Strip_All_Html() + { + var text = "Hello world, this is some text with a link"; + + 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, this is some text with a link"; + + string [] tags = {"b"}; + + var helper = new UmbracoHelper(); + + var result = helper.StripHtml(text, tags).ToString(); + + Assert.AreEqual("Hello world, this is some text with a link", result); + } + + [Test] + public void Strip_Invalid_Html() + { + var text = "Hello world, is some text with a link"; + + var helper = new UmbracoHelper(); + + var result = helper.StripHtml(text).ToString(); + + Assert.AreEqual("Hello world, is some text with a link", result); + } } } diff --git a/src/Umbraco.Tests/DateTimeExtensionsTests.cs b/src/Umbraco.Tests/Misc/DateTimeExtensionsTests.cs similarity index 98% rename from src/Umbraco.Tests/DateTimeExtensionsTests.cs rename to src/Umbraco.Tests/Misc/DateTimeExtensionsTests.cs index 83a47b3551..99d3943a04 100644 --- a/src/Umbraco.Tests/DateTimeExtensionsTests.cs +++ b/src/Umbraco.Tests/Misc/DateTimeExtensionsTests.cs @@ -2,7 +2,7 @@ using NUnit.Framework; using Umbraco.Core; -namespace Umbraco.Tests +namespace Umbraco.Tests.Misc { [TestFixture] public class DateTimeExtensionsTests diff --git a/src/Umbraco.Tests/Misc/HashGeneratorTests.cs b/src/Umbraco.Tests/Misc/HashGeneratorTests.cs new file mode 100644 index 0000000000..b1c3fedf92 --- /dev/null +++ b/src/Umbraco.Tests/Misc/HashGeneratorTests.cs @@ -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()); + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Tests/Models/UserExtensionsTests.cs b/src/Umbraco.Tests/Models/UserExtensionsTests.cs index 9c776a3c00..14d8bb5f17 100644 --- a/src/Umbraco.Tests/Models/UserExtensionsTests.cs +++ b/src/Umbraco.Tests/Models/UserExtensionsTests.cs @@ -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 diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs index 2d1dba886e..e1757fe7cb 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs @@ -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(), Mock.Of(), Mock.Of()); - 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()); + cacheHelper = cacheHelper ?? CacheHelper; + + templateRepository = new TemplateRepository(unitOfWork, cacheHelper, Logger, Mock.Of(), Mock.Of(), Mock.Of()); + 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()); 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)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)); } diff --git a/src/Umbraco.Tests/Persistence/Repositories/DictionaryRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/DictionaryRepositoryTest.cs index 324a98c705..98afb80c13 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/DictionaryRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/DictionaryRepositoryTest.cs @@ -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 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() { diff --git a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs index d5368241ca..60c1aaae82 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs @@ -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()); + 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()); 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)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)); } } diff --git a/src/Umbraco.Tests/Persistence/Repositories/SimilarNodeNameTests.cs b/src/Umbraco.Tests/Persistence/Repositories/SimilarNodeNameTests.cs new file mode 100644 index 0000000000..2b9e3272ef --- /dev/null +++ b/src/Umbraco.Tests/Persistence/Repositories/SimilarNodeNameTests.cs @@ -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)); + } + } +} diff --git a/src/Umbraco.Tests/Persistence/SqlCeTableByTableTest.cs b/src/Umbraco.Tests/Persistence/SqlCeTableByTableTest.cs index 76849cc95f..0395095134 100644 --- a/src/Umbraco.Tests/Persistence/SqlCeTableByTableTest.cs +++ b/src/Umbraco.Tests/Persistence/SqlCeTableByTableTest.cs @@ -541,6 +541,7 @@ namespace Umbraco.Tests.Persistence { var helper = new DatabaseSchemaHelper(scope.Database, Mock.Of()); + helper.CreateTable(); helper.CreateTable(); scope.Complete(); diff --git a/src/Umbraco.Tests/Plugins/PluginManagerTests.cs b/src/Umbraco.Tests/Plugins/PluginManagerTests.cs index 7c1a0a9417..0b13d34204 100644 --- a/src/Umbraco.Tests/Plugins/PluginManagerTests.cs +++ b/src/Umbraco.Tests/Plugins/PluginManagerTests.cs @@ -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() { diff --git a/src/Umbraco.Tests/Routing/ContentFinderByNiceUrlTests.cs b/src/Umbraco.Tests/Routing/ContentFinderByNiceUrlTests.cs index 0a1cda7891..b932912242 100644 --- a/src/Umbraco.Tests/Routing/ContentFinderByNiceUrlTests.cs +++ b/src/Umbraco.Tests/Routing/ContentFinderByNiceUrlTests.cs @@ -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); + } + /// + /// This test handles requests with special characters in the URL. + /// + /// + /// + [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); + } + + /// + /// 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. + /// + /// + /// + [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); + } + + /// + /// 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. + /// + /// + /// + [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); } diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index 89760d11e9..e94b010c27 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -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); } + + /// + /// 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. + /// + [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(); diff --git a/src/Umbraco.Tests/Services/EntityServiceTests.cs b/src/Umbraco.Tests/Services/EntityServiceTests.cs index fa31da0e3c..aa8f68febf 100644 --- a/src/Umbraco.Tests/Services/EntityServiceTests.cs +++ b/src/Umbraco.Tests/Services/EntityServiceTests.cs @@ -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; diff --git a/src/Umbraco.Tests/Services/MemberServiceTests.cs b/src/Umbraco.Tests/Services/MemberServiceTests.cs index a5f48e348c..9b1b57e9a0 100644 --- a/src/Umbraco.Tests/Services/MemberServiceTests.cs +++ b/src/Umbraco.Tests/Services/MemberServiceTests.cs @@ -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(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(() => ServiceContext.MemberService.SavePassword(member, "HELLO123456$!")); + } + [Test] public void Can_Create_Member() { diff --git a/src/Umbraco.Tests/Services/ThreadSafetyServiceTest.cs b/src/Umbraco.Tests/Services/ThreadSafetyServiceTest.cs index 5198f8cd67..1e4668d8da 100644 --- a/src/Umbraco.Tests/Services/ThreadSafetyServiceTest.cs +++ b/src/Umbraco.Tests/Services/ThreadSafetyServiceTest.cs @@ -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; diff --git a/src/Umbraco.Tests/Services/UserServiceTests.cs b/src/Umbraco.Tests/Services/UserServiceTests.cs index f47ccfd043..894e69a7ca 100644 --- a/src/Umbraco.Tests/Services/UserServiceTests.cs +++ b/src/Umbraco.Tests/Services/UserServiceTests.cs @@ -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(() => userService.CreateUserWithIdentity(string.Empty, "john@umbraco.io")); } diff --git a/src/Umbraco.Tests/Strings/StringExtensionsTests.cs b/src/Umbraco.Tests/Strings/StringExtensionsTests.cs index be64ecbbd6..c4346ba50c 100644 --- a/src/Umbraco.Tests/Strings/StringExtensionsTests.cs +++ b/src/Umbraco.Tests/Strings/StringExtensionsTests.cs @@ -27,7 +27,7 @@ namespace Umbraco.Tests.Strings var helper = Current.ShortStringHelper; Assert.IsInstanceOf(helper); } - + [TestCase("hello", "world", false)] [TestCase("hello", "hello", true)] [TestCase("hellohellohellohellohellohellohello", "hellohellohellohellohellohellohelloo", false)] diff --git a/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs b/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs index 9af5fa1801..73518156b5 100644 --- a/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs +++ b/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs @@ -53,6 +53,8 @@ namespace Umbraco.Tests.TestHelpers + + diff --git a/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs index 65d114793d..54d50cf953 100644 --- a/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs +++ b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs @@ -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(); + var mockedContentService = Mock.Of(); + var mockedMediaService = Mock.Of(); + var mockedEntityService = Mock.Of(); var mockedMigrationService = new Mock(); //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(), sectionService:Mock.Of()); @@ -86,10 +93,17 @@ namespace Umbraco.Tests.TestHelpers.ControllerTesting var webSecurity = new Mock(null, null); //mock CurrentUser + var groups = new List(); + 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(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(), It.IsAny())) + webSecurity.Setup(x => x.UserHasSectionAccess(It.IsAny(), It.IsAny())) .Returns(() => true); var umbCtx = UmbracoContext.EnsureContext( diff --git a/src/Umbraco.Tests/TestHelpers/Entities/MockedMember.cs b/src/Umbraco.Tests/TestHelpers/Entities/MockedMember.cs index 0bbaea224b..9817578617 100644 --- a/src/Umbraco.Tests/TestHelpers/Entities/MockedMember.cs +++ b/src/Umbraco.Tests/TestHelpers/Entities/MockedMember.cs @@ -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 CreateSimpleMember(IMemberType memberType, int amount, Action onCreating = null) { var list = new List(); diff --git a/src/Umbraco.Tests/TestHelpers/TestObjects.cs b/src/Umbraco.Tests/TestHelpers/TestObjects.cs index 53dedfd733..83e0babf64 100644 --- a/src/Umbraco.Tests/TestHelpers/TestObjects.cs +++ b/src/Umbraco.Tests/TestHelpers/TestObjects.cs @@ -157,12 +157,12 @@ namespace Umbraco.Tests.TestHelpers var userService = GetLazyService(container, () => new UserService(provider, logger, eventMessagesFactory, runtimeState)); var dataTypeService = GetLazyService(container, () => new DataTypeService(provider, logger, eventMessagesFactory)); - var contentService = GetLazyService(container, () => new ContentService(provider, logger, eventMessagesFactory, mediaFileSystem, idkMap)); + var contentService = GetLazyService(container, () => new ContentService(provider, logger, eventMessagesFactory, mediaFileSystem)); var notificationService = GetLazyService(container, () => new NotificationService(provider, userService.Value, contentService.Value, logger)); var serverRegistrationService = GetLazyService(container, () => new ServerRegistrationService(provider, logger, eventMessagesFactory)); var memberGroupService = GetLazyService(container, () => new MemberGroupService(provider, logger, eventMessagesFactory)); var memberService = GetLazyService(container, () => new MemberService(provider, logger, eventMessagesFactory, memberGroupService.Value, mediaFileSystem)); - var mediaService = GetLazyService(container, () => new MediaService(provider, mediaFileSystem, logger, eventMessagesFactory, idkMap)); + var mediaService = GetLazyService(container, () => new MediaService(provider, mediaFileSystem, logger, eventMessagesFactory)); var contentTypeService = GetLazyService(container, () => new ContentTypeService(provider, logger, eventMessagesFactory, contentService.Value)); var mediaTypeService = GetLazyService(container, () => new MediaTypeService(provider, logger, eventMessagesFactory, mediaService.Value)); var fileService = GetLazyService(container, () => new FileService(provider, logger, eventMessagesFactory)); diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index c6bfcf0248..cd022e031a 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -218,7 +218,8 @@ - + + @@ -227,6 +228,7 @@ + @@ -285,8 +287,11 @@ + + + diff --git a/src/Umbraco.Tests/Web/Controllers/UserEditorAuthorizationHelperTests.cs b/src/Umbraco.Tests/Web/Controllers/UserEditorAuthorizationHelperTests.cs new file mode 100644 index 0000000000..b46e92d553 --- /dev/null +++ b/src/Umbraco.Tests/Web/Controllers/UserEditorAuthorizationHelperTests.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(); + + var contentService = new Mock(); + var mediaService = new Mock(); + var userService = new Mock(); + var entityService = new Mock(); + + 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(); + var savingUser = GetAdminUser(); + + var contentService = new Mock(); + var mediaService = new Mock(); + var userService = new Mock(); + var entityService = new Mock(); + + 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(user => user.Groups == new[] + { + new ReadOnlyUserGroup(1, "Test", "icon-user", null, null, "test", new string[0], new string[0]) + }); + var savingUser = Mock.Of(); + + var contentService = new Mock(); + var mediaService = new Mock(); + var userService = new Mock(); + var entityService = new Mock(); + + 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(user => user.Groups == new[] + { + new ReadOnlyUserGroup(1, "Test", "icon-user", null, null, "test", new string[0], new string[0]) + }); + var savingUser = Mock.Of(); + + var contentService = new Mock(); + var mediaService = new Mock(); + var userService = new Mock(); + var entityService = new Mock(); + + 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 + { + {1234, "-1,1234"}, + {9876, "-1,9876"}, + {5555, "-1,9876,5555"}, + {4567, "-1,4567"}, + }; + + var currentUser = Mock.Of(user => user.StartContentIds == new[] { 9876 }); + var savingUser = Mock.Of(user => user.StartContentIds == new[] {1234}); + + var contentService = new Mock(); + contentService.Setup(x => x.GetById(It.IsAny())) + .Returns((int id) => Mock.Of(content => content.Path == nodePaths[id])); + var mediaService = new Mock(); + var userService = new Mock(); + var entityService = new Mock(); + entityService.Setup(service => service.GetAllPaths(It.IsAny(), It.IsAny())) + .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 + { + {1234, "-1,1234"}, + {9876, "-1,9876"}, + {5555, "-1,9876,5555"}, + {4567, "-1,4567"}, + }; + + var currentUser = Mock.Of(user => user.StartContentIds == new[] { 9876 }); + var savingUser = Mock.Of(user => user.StartContentIds == new[] { 1234, 4567 }); + + var contentService = new Mock(); + contentService.Setup(x => x.GetById(It.IsAny())) + .Returns((int id) => Mock.Of(content => content.Path == nodePaths[id])); + var mediaService = new Mock(); + var userService = new Mock(); + var entityService = new Mock(); + entityService.Setup(service => service.GetAllPaths(It.IsAny(), It.IsAny())) + .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 + { + {1234, "-1,1234"}, + {9876, "-1,9876"}, + {5555, "-1,9876,5555"}, + {4567, "-1,4567"}, + }; + + var currentUser = Mock.Of(user => user.StartContentIds == new[]{9876}); + var savingUser = Mock.Of(); + + var contentService = new Mock(); + contentService.Setup(x => x.GetById(It.IsAny())) + .Returns((int id) => Mock.Of(content => content.Path == nodePaths[id])); + var mediaService = new Mock(); + var userService = new Mock(); + var entityService = new Mock(); + entityService.Setup(service => service.GetAllPaths(It.IsAny(), It.IsAny())) + .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 + { + {1234, "-1,1234"}, + {9876, "-1,9876"}, + {5555, "-1,9876,5555"}, + {4567, "-1,4567"}, + }; + + var currentUser = Mock.Of(user => user.StartContentIds == new[] { 9876 }); + var savingUser = Mock.Of(); + + var contentService = new Mock(); + contentService.Setup(x => x.GetById(It.IsAny())) + .Returns((int id) => Mock.Of(content => content.Path == nodePaths[id])); + var mediaService = new Mock(); + var userService = new Mock(); + var entityService = new Mock(); + entityService.Setup(service => service.GetAllPaths(It.IsAny(), It.IsAny())) + .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 + { + {1234, "-1,1234"}, + {9876, "-1,9876"}, + {5555, "-1,9876,5555"}, + {4567, "-1,4567"}, + }; + + var currentUser = Mock.Of(user => user.StartMediaIds == new[] { 9876 }); + var savingUser = Mock.Of(); + + var contentService = new Mock(); + var mediaService = new Mock(); + mediaService.Setup(x => x.GetById(It.IsAny())) + .Returns((int id) => Mock.Of(content => content.Path == nodePaths[id])); + var userService = new Mock(); + var entityService = new Mock(); + entityService.Setup(service => service.GetAllPaths(It.IsAny(), It.IsAny())) + .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 + { + {1234, "-1,1234"}, + {9876, "-1,9876"}, + {5555, "-1,9876,5555"}, + {4567, "-1,4567"}, + }; + + var currentUser = Mock.Of(user => user.StartMediaIds == new[] { 9876 }); + var savingUser = Mock.Of(); + + var contentService = new Mock(); + var mediaService = new Mock(); + mediaService.Setup(x => x.GetById(It.IsAny())) + .Returns((int id) => Mock.Of(content => content.Path == nodePaths[id])); + var userService = new Mock(); + var entityService = new Mock(); + entityService.Setup(service => service.GetAllPaths(It.IsAny(), It.IsAny())) + .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 + { + {1234, "-1,1234"}, + {9876, "-1,9876"}, + {5555, "-1,9876,5555"}, + {4567, "-1,4567"}, + }; + + var currentUser = Mock.Of(user => user.StartMediaIds == new[] { 9876 }); + var savingUser = Mock.Of(user => user.StartMediaIds == new[] { 1234 }); + + var contentService = new Mock(); + var mediaService = new Mock(); + mediaService.Setup(x => x.GetById(It.IsAny())) + .Returns((int id) => Mock.Of(content => content.Path == nodePaths[id])); + var userService = new Mock(); + var entityService = new Mock(); + entityService.Setup(service => service.GetAllPaths(It.IsAny(), It.IsAny())) + .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 + { + {1234, "-1,1234"}, + {9876, "-1,9876"}, + {5555, "-1,9876,5555"}, + {4567, "-1,4567"}, + }; + + var currentUser = Mock.Of(user => user.StartMediaIds == new[] { 9876 }); + var savingUser = Mock.Of(user => user.StartMediaIds == new[] { 1234, 4567 }); + + var contentService = new Mock(); + var mediaService = new Mock(); + mediaService.Setup(x => x.GetById(It.IsAny())) + .Returns((int id) => Mock.Of(content => content.Path == nodePaths[id])); + var userService = new Mock(); + var entityService = new Mock(); + entityService.Setup(service => service.GetAllPaths(It.IsAny(), It.IsAny())) + .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(user => user.Groups == new[] + { + new ReadOnlyUserGroup(1, "Admin", "icon-user", null, null, Constants.Security.AdminGroupAlias, new string[0], new string[0]) + }); + return admin; + } + } +} diff --git a/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs b/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs index bbfb8357ec..d81c9351be 100644 --- a/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs +++ b/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs @@ -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(), It.IsAny(), out outVal, It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + userServiceMock.Setup(service => service.GetAll( + It.IsAny(), It.IsAny(), out outVal, It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())) .Returns(() => users); //we need to manually apply automapper mappings with the mocked applicationcontext diff --git a/src/Umbraco.Tests/Web/HttpCookieExtensionsTests.cs b/src/Umbraco.Tests/Web/HttpCookieExtensionsTests.cs new file mode 100644 index 0000000000..f13b94a669 --- /dev/null +++ b/src/Umbraco.Tests/Web/HttpCookieExtensionsTests.cs @@ -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); + } + } + } +} diff --git a/src/Umbraco.Tests/Web/Mvc/HtmlStringUtilitiesTests.cs b/src/Umbraco.Tests/Web/Mvc/HtmlStringUtilitiesTests.cs new file mode 100644 index 0000000000..c84d578d14 --- /dev/null +++ b/src/Umbraco.Tests/Web/Mvc/HtmlStringUtilitiesTests.cs @@ -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("

hello world

hello world\r\nhello world\rhello world\nhello world

"); + var expected = "

hello world

hello world
hello world
hello world
hello world

"; + Assert.AreEqual(expected, output); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Cache/ContentCacheRefresher.cs b/src/Umbraco.Web/Cache/ContentCacheRefresher.cs index 4c8d15eab4..258c8df86d 100644 --- a/src/Umbraco.Web/Cache/ContentCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/ContentCacheRefresher.cs @@ -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) { diff --git a/src/Umbraco.Web/Cache/TemplateCacheRefresher.cs b/src/Umbraco.Web/Cache/TemplateCacheRefresher.cs index 66289b29fc..fa89ba0854 100644 --- a/src/Umbraco.Web/Cache/TemplateCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/TemplateCacheRefresher.cs @@ -7,7 +7,7 @@ namespace Umbraco.Web.Cache { public sealed class TemplateCacheRefresher : CacheRefresherBase { - private readonly IdkMap _idkMap; + private readonly IdkMap _idkMap; public TemplateCacheRefresher(CacheHelper cacheHelper, IdkMap idkMap) : base(cacheHelper) diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 905c9fa519..b9f77b7c45 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -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) diff --git a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs index 66aaccd96a..1b249cf68c 100644 --- a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs @@ -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}, } }, { diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index ef61c17c57..cc18aeccae 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -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(blueprint); diff --git a/src/Umbraco.Web/Editors/ContentPostValidateAttribute.cs b/src/Umbraco.Web/Editors/ContentPostValidateAttribute.cs index cc4de58300..cc46b6da19 100644 --- a/src/Umbraco.Web/Editors/ContentPostValidateAttribute.cs +++ b/src/Umbraco.Web/Editors/ContentPostValidateAttribute.cs @@ -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); diff --git a/src/Umbraco.Web/Editors/ContentTypeController.cs b/src/Umbraco.Web/Editors/ContentTypeController.cs index ff21203d68..761d0fdb3a 100644 --- a/src/Umbraco.Web/Editors/ContentTypeController.cs +++ b/src/Umbraco.Web/Editors/ContentTypeController.cs @@ -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( @@ -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; } diff --git a/src/Umbraco.Web/Editors/CurrentUserController.cs b/src/Umbraco.Web/Editors/CurrentUserController.cs index f97f389c9d..6c34a198d3 100644 --- a/src/Umbraco.Web/Editors/CurrentUserController.cs +++ b/src/Umbraco.Web/Editors/CurrentUserController.cs @@ -80,7 +80,7 @@ namespace Umbraco.Web.Editors public async Task> 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)); } diff --git a/src/Umbraco.Web/Editors/DashboardController.cs b/src/Umbraco.Web/Editors/DashboardController.cs index 949a2752a9..e0c43a085e 100644 --- a/src/Umbraco.Web/Editors/DashboardController.cs +++ b/src/Umbraco.Web/Editors/DashboardController.cs @@ -123,54 +123,8 @@ namespace Umbraco.Web.Editors [ValidateAngularAntiForgeryToken] public IEnumerable> GetDashboard(string section) { - var tabs = new List>(); - 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(); - - 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 - { - 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); } } } diff --git a/src/Umbraco.Web/Editors/DashboardHelper.cs b/src/Umbraco.Web/Editors/DashboardHelper.cs new file mode 100644 index 0000000000..8b43335ebe --- /dev/null +++ b/src/Umbraco.Web/Editors/DashboardHelper.cs @@ -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; + } + + /// + /// Returns the dashboard models per section for the current user and it's access + /// + /// + /// + public IDictionary>> GetDashboards(IUser currentUser) + { + var result = new Dictionary>>(); + foreach (var section in _sectionService.GetSections()) + { + result[section.Alias] = GetDashboard(section.Alias, currentUser); + } + return result; + } + + /// + /// Returns the dashboard model for the given section based on the current user and it's access + /// + /// + /// + /// + public IEnumerable> GetDashboard(string section, IUser currentUser) + { + var tabs = new List>(); + 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(); + + 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 + { + 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; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/DataTypeController.cs b/src/Umbraco.Web/Editors/DataTypeController.cs index 77c392bf8c..1fa31a7b43 100644 --- a/src/Umbraco.Web/Editors/DataTypeController.cs +++ b/src/Umbraco.Web/Editors/DataTypeController.cs @@ -20,7 +20,7 @@ using Umbraco.Web.Composing; namespace Umbraco.Web.Editors { - + /// /// The API controller used for editing data types /// @@ -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 /// /// 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(); ; } diff --git a/src/Umbraco.Web/Editors/MediaTypeController.cs b/src/Umbraco.Web/Editors/MediaTypeController.cs index 24e5b0a3fa..00a5def3b9 100644 --- a/src/Umbraco.Web/Editors/MediaTypeController.cs +++ b/src/Umbraco.Web/Editors/MediaTypeController.cs @@ -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( diff --git a/src/Umbraco.Web/Editors/PasswordChanger.cs b/src/Umbraco.Web/Editors/PasswordChanger.cs index ad62c0b0e9..cc4a7d8c1f 100644 --- a/src/Umbraco.Web/Editors/PasswordChanger.cs +++ b/src/Umbraco.Web/Editors/PasswordChanger.cs @@ -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; } + /// + /// Changes the password for a user based on the many different rules and config options + /// + /// The user performing the password save action + /// The user who's password is being changed + /// + /// + /// public async Task> ChangePasswordWithIdentityAsync( IUser currentUser, + IUser savingUser, ChangingPasswordModel passwordModel, - ModelStateDictionary modelState, BackOfficeUserManager 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($"Could not reset member password {errors}"); + _logger.Warn($"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($"Could not change member password {errors}"); - return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, errors: " + errors, new[] { "value" }) }); + _logger.Warn($"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 /// /// Changes password for a member/user given the membership provider and the password change model /// - /// + /// The username of the user having their password changed /// /// /// diff --git a/src/Umbraco.Web/Editors/SectionController.cs b/src/Umbraco.Web/Editors/SectionController.cs index d527724abf..0a0e8397cb 100644 --- a/src/Umbraco.Web/Editors/SectionController.cs +++ b/src/Umbraco.Web/Editors/SectionController.cs @@ -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
GetSections() { + var sections = Services.SectionService.GetAllowedSections(Security.GetUserId()); - return sections.Select(Mapper.Map); + + var sectionModels = sections.Select(Mapper.Map).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> 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; } + /// + /// Returns all the sections that the user has access to + /// + /// public IEnumerable
GetAllSections() { var sections = Services.SectionService.GetSections(); - return sections.Select(Mapper.Map); + var mapped = sections.Select(Mapper.Map); + if (Security.CurrentUser.IsAdmin()) + return mapped; + + return mapped.Where(x => Security.CurrentUser.AllowedSections.Contains(x.Alias)).ToArray(); } } diff --git a/src/Umbraco.Web/Editors/UserEditorAuthorizationHelper.cs b/src/Umbraco.Web/Editors/UserEditorAuthorizationHelper.cs new file mode 100644 index 0000000000..5dcd5572e4 --- /dev/null +++ b/src/Umbraco.Web/Editors/UserEditorAuthorizationHelper.cs @@ -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; + } + + /// + /// Checks if the current user has access to save the user data + /// + /// The current user trying to save user data + /// The user instance being saved (can be null if it's a new user) + /// The start content ids of the user being saved (can be null or empty) + /// The start media ids of the user being saved (can be null or empty) + /// The user aliases of the user being saved (can be null or empty) + /// + public Attempt IsAuthorized(IUser currentUser, + IUser savingUser, + IEnumerable startContentIds, IEnumerable startMediaIds, + IEnumerable 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.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.Succeed(); + } + + private Attempt AuthorizePath(IUser currentUser, IEnumerable startContentIds, IEnumerable 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.Succeed(); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/UserGroupAuthorizationAttribute.cs b/src/Umbraco.Web/Editors/UserGroupAuthorizationAttribute.cs new file mode 100644 index 0000000000..00297fc2cd --- /dev/null +++ b/src/Umbraco.Web/Editors/UserGroupAuthorizationAttribute.cs @@ -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 +{ + /// + /// Authorizes that the current user has access to the user group Id in the request + /// + internal class UserGroupAuthorizationAttribute : AuthorizeAttribute + { + private readonly string _paramName; + private readonly UmbracoContext _umbracoContext; + + /// + /// THIS SHOULD BE ONLY USED FOR UNIT TESTS + /// + /// + /// + 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()).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); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/UserGroupEditorAuthorizationHelper.cs b/src/Umbraco.Web/Editors/UserGroupEditorAuthorizationHelper.cs new file mode 100644 index 0000000000..6bc39b376d --- /dev/null +++ b/src/Umbraco.Web/Editors/UserGroupEditorAuthorizationHelper.cs @@ -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; + } + + /// + /// Authorize that the current user belongs to these groups + /// + /// + /// + /// + public Attempt AuthorizeGroupAccess(IUser currentUser, params int[] groupIds) + { + if (currentUser.IsAdmin()) + return Attempt.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.Succeed() + : Attempt.Fail("User is not a member of " + string.Join(", ", missingAccess)); + } + + /// + /// Authorize that the current user belongs to these groups + /// + /// + /// + /// + public Attempt AuthorizeGroupAccess(IUser currentUser, params string[] groupAliases) + { + if (currentUser.IsAdmin()) + return Attempt.Succeed(); + + var userGroups = currentUser.Groups.Select(x => x.Alias).ToArray(); + var missingAccess = groupAliases.Except(userGroups).ToArray(); + return missingAccess.Length == 0 + ? Attempt.Succeed() + : Attempt.Fail("User is not a member of " + string.Join(", ", missingAccess)); + } + + /// + /// Authorize that the user is not adding a section to the group that they don't have access to + /// + /// + /// + /// + /// + public Attempt AuthorizeSectionChanges(IUser currentUser, + IEnumerable currentAllowedSections, + IEnumerable proposedAllowedSections) + { + if (currentUser.IsAdmin()) + return Attempt.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.Succeed(); + } + + /// + /// Authorize that the user is not changing to a start node that they don't have access to (including admins) + /// + /// + /// + /// + /// + /// + /// + public Attempt 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.Succeed(); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/UserGroupsController.cs b/src/Umbraco.Web/Editors/UserGroupsController.cs index ad896fe500..0922d36dbe 100644 --- a/src/Umbraco.Web/Editors/UserGroupsController.cs +++ b/src/Umbraco.Web/Editors/UserGroupsController.cs @@ -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 ///
/// - public IEnumerable GetUserGroups() + public IEnumerable GetUserGroups(bool onlyCurrentUserGroups = true) { - return Mapper.Map, IEnumerable>(Services.UserService.GetAllUserGroups()); + var allGroups = Mapper.Map, IEnumerable>(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(); } /// /// Return a user group /// /// + [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); diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs index 83c1484c74..785710fc61 100644 --- a/src/Umbraco.Web/Editors/UsersController.cs +++ b/src/Umbraco.Web/Editors/UsersController.cs @@ -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(user); + var result = Mapper.Map(user); + return result; } - - /// /// Returns a paged users collection /// @@ -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(); + + //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>(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 /// /// Invites a user /// - /// + /// /// /// /// 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 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 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(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; - } + } /// /// 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 })); + } + + /// + /// Unlocks the users with the given user ids + /// + /// + public async Task 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 UserStates { get; set; } } + } } \ No newline at end of file diff --git a/src/Umbraco.Web/HealthCheck/Checks/Config/CustomErrorsCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Config/CustomErrorsCheck.cs index cbf761b581..63986b6c62 100644 --- a/src/Umbraco.Web/HealthCheck/Checks/Config/CustomErrorsCheck.cs +++ b/src/Umbraco.Web/HealthCheck/Checks/Config/CustomErrorsCheck.cs @@ -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 }); } } diff --git a/src/Umbraco.Web/HealthCheck/Checks/Permissions/FolderAndFilePermissionsCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Permissions/FolderAndFilePermissionsCheck.cs index ae3db90660..86cef17b4c 100644 --- a/src/Umbraco.Web/HealthCheck/Checks/Permissions/FolderAndFilePermissionsCheck.cs +++ b/src/Umbraco.Web/HealthCheck/Checks/Permissions/FolderAndFilePermissionsCheck.cs @@ -59,12 +59,10 @@ namespace Umbraco.Web.HealthCheck.Checks.Permissions // in ALL circumstances or just some var pathsToCheck = new Dictionary { - { 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 + { + { SystemDirectories.AppCode, PermissionCheckRequirement.Optional }, + { SystemDirectories.Bin, PermissionCheckRequirement.Optional } + }; + // Run checks for required and optional paths for modify permission - IEnumerable requiredFailedPaths; - IEnumerable 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); } diff --git a/src/Umbraco.Web/HealthCheck/NotificationMethods/EmailNotificationMethod.cs b/src/Umbraco.Web/HealthCheck/NotificationMethods/EmailNotificationMethod.cs index 392d166d68..e9b3f4dabf 100644 --- a/src/Umbraco.Web/HealthCheck/NotificationMethods/EmailNotificationMethod.cs +++ b/src/Umbraco.Web/HealthCheck/NotificationMethods/EmailNotificationMethod.cs @@ -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); } } diff --git a/src/Umbraco.Web/HealthCheck/NotificationMethods/IHealthCheckNotificationMethod.cs b/src/Umbraco.Web/HealthCheck/NotificationMethods/IHealthCheckNotificationMethod.cs index 17102014bd..2b8009d201 100644 --- a/src/Umbraco.Web/HealthCheck/NotificationMethods/IHealthCheckNotificationMethod.cs +++ b/src/Umbraco.Web/HealthCheck/NotificationMethods/IHealthCheckNotificationMethod.cs @@ -5,6 +5,7 @@ namespace Umbraco.Web.HealthCheck.NotificationMethods { public interface IHealthCheckNotificationMethod { + bool Enabled { get; } Task SendAsync(HealthCheckResults results, CancellationToken token); } } diff --git a/src/Umbraco.Web/HtmlStringUtilities.cs b/src/Umbraco.Web/HtmlStringUtilities.cs index 4ecf7c61ca..c0c4efe96d 100644 --- a/src/Umbraco.Web/HtmlStringUtilities.cs +++ b/src/Umbraco.Web/HtmlStringUtilities.cs @@ -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 /// The text with text line breaks replaced with html linebreaks (
)
public string ReplaceLineBreaksForHtml(string text) { - return text.Replace("\n", "
\n"); + return text.Replace("\r\n", @"
").Replace("\n", @"
").Replace("\r", @"
"); } public HtmlString StripHtmlTags(string html, params string[] tags) { var doc = new HtmlDocument(); doc.LoadHtml("

" + html + "

"); + var targets = new List(); 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 = "…"; + 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(""); + 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("…"); + 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); } } } } + + /// + /// Returns the length of the words from a html block + /// + /// Html text + /// Amount of words you would like to measure + /// + /// + 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; + } } } diff --git a/src/Umbraco.Web/HttpCookieExtensions.cs b/src/Umbraco.Web/HttpCookieExtensions.cs index 9206a3c3ad..29c82955ea 100644 --- a/src/Umbraco.Web/HttpCookieExtensions.cs +++ b/src/Umbraco.Web/HttpCookieExtensions.cs @@ -16,6 +16,40 @@ namespace Umbraco.Web /// internal static class HttpCookieExtensions { + /// + /// Retrieves an individual cookie from the cookies collection + /// + /// + /// + /// + /// + /// 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. + /// + 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; + } + /// /// Removes the cookie from the request and the response if it exists /// diff --git a/src/Umbraco.Web/Install/Controllers/InstallController.cs b/src/Umbraco.Web/Install/Controllers/InstallController.cs index acdb8d98b1..e03c3fe531 100644 --- a/src/Umbraco.Web/Install/Controllers/InstallController.cs +++ b/src/Umbraco.Web/Install/Controllers/InstallController.cs @@ -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); diff --git a/src/Umbraco.Web/Install/FilePermissionHelper.cs b/src/Umbraco.Web/Install/FilePermissionHelper.cs index 5b160f482b..a2c67d34c6 100644 --- a/src/Umbraco.Web/Install/FilePermissionHelper.cs +++ b/src/Umbraco.Web/Install/FilePermissionHelper.cs @@ -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 errors) + /// + /// This will test the directories for write access + /// + /// + /// + /// + /// 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. + /// + /// + public static bool EnsureDirectories(string[] dirs, out IEnumerable errors, bool writeCausesRestart = false) { List 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(); @@ -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) diff --git a/src/Umbraco.Web/Models/ChangingPasswordModel.cs b/src/Umbraco.Web/Models/ChangingPasswordModel.cs index 6778b8c4eb..33f14d97b2 100644 --- a/src/Umbraco.Web/Models/ChangingPasswordModel.cs +++ b/src/Umbraco.Web/Models/ChangingPasswordModel.cs @@ -20,8 +20,21 @@ namespace Umbraco.Web.Models public string OldPassword { get; set; } /// - /// 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 /// + /// + /// + /// This operator is different between using ASP.NET Identity APIs and Membership APIs. + /// + /// + /// When using Membership APIs, this is only valid when: EnablePasswordReset = true and it will reset the password to something auto generated. + /// + /// + /// 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. + /// + /// [DataMember(Name = "reset")] public bool? Reset { get; set; } diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs index 4870bd1cc6..695455be5d 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs @@ -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 /// [DataMember(Name = "allowedActions")] public IEnumerable AllowedActions { get; set; } - + + [DataMember(Name = "isBlueprint")] + public bool IsBlueprint { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/Section.cs b/src/Umbraco.Web/Models/ContentEditing/Section.cs index b0c1839297..555b4a7cc1 100644 --- a/src/Umbraco.Web/Models/ContentEditing/Section.cs +++ b/src/Umbraco.Web/Models/ContentEditing/Section.cs @@ -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 { - /// /// Represents a section (application) in the back office /// [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; } + /// + /// 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 + /// + [DataMember(Name = "routePath")] + public string RoutePath { get; set; } } } diff --git a/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs b/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs index e29b58fd6f..70a3a41133 100644 --- a/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs +++ b/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs @@ -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; } /// /// Gets/sets the number of seconds for the user's auth ticket to expire diff --git a/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs index 4cff43e3b8..8a79344c8e 100644 --- a/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs @@ -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; } - + + /// + /// A readonly value showing the user's current calculated start content ids + /// + [DataMember(Name = "calculatedStartContentIds")] + [ReadOnly(true)] + public IEnumerable CalculatedStartContentIds { get; set; } + + /// + /// A readonly value showing the user's current calculated start media ids + /// + [DataMember(Name = "calculatedStartMediaIds")] + [ReadOnly(true)] + public IEnumerable 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; } + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/UserInvite.cs b/src/Umbraco.Web/Models/ContentEditing/UserInvite.cs index 06895ccc68..368067814d 100644 --- a/src/Umbraco.Web/Models/ContentEditing/UserInvite.cs +++ b/src/Umbraco.Web/Models/ContentEditing/UserInvite.cs @@ -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 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" }); } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Models/LoginStatusModel.cs b/src/Umbraco.Web/Models/LoginStatusModel.cs index c6294e8881..78425969dd 100644 --- a/src/Umbraco.Web/Models/LoginStatusModel.cs +++ b/src/Umbraco.Web/Models/LoginStatusModel.cs @@ -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) { diff --git a/src/Umbraco.Web/Models/Mapping/ContentProfile.cs b/src/Umbraco.Web/Models/Mapping/ContentProfile.cs index d00138de72..4b063460f6 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentProfile.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentProfile.cs @@ -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() - .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 CreateMap>() - .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 CreateMap>() - .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()) diff --git a/src/Umbraco.Web/Models/Mapping/SectionProfile.cs b/src/Umbraco.Web/Models/Mapping/SectionProfile.cs index 78d5e40675..ea528026c5 100644 --- a/src/Umbraco.Web/Models/Mapping/SectionProfile.cs +++ b/src/Umbraco.Web/Models/Mapping/SectionProfile.cs @@ -14,9 +14,8 @@ namespace Umbraco.Web.Models.Mapping _textService = textService; CreateMap() - .ForMember( - dto => dto.Name, - expression => expression.MapFrom(section => _textService.Localize("sections/" + section.Alias, (IDictionary)null))) + .ForMember(dest => dest.RoutePath, opt => opt.Ignore()) + .ForMember(dest => dest.Name, opt => opt.MapFrom(src => _textService.Localize("sections/" + src.Alias, (IDictionary)null))) .ReverseMap(); //backwards too! } } diff --git a/src/Umbraco.Web/Models/Mapping/UserProfile.cs b/src/Umbraco.Web/Models/Mapping/UserProfile.cs index 5ca4a6ada5..76b19ecf3d 100644 --- a/src/Umbraco.Web/Models/Mapping/UserProfile.cs +++ b/src/Umbraco.Web/Models/Mapping/UserProfile.cs @@ -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() .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())) - .ForMember(dest => dest.StartMediaIds, opt => opt.UseValue(Enumerable.Empty())) + .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(); - if (startContentIds.Contains(-1)) - { - startNodes.Add(RootNode(textService.Localize("content/contentRoot"))); - } - var contentItems = entityService.GetAll(UmbracoObjectTypes.Document, startContentIds); - startNodes.AddRange(Mapper.Map, IEnumerable>(contentItems)); - display.StartContentIds = startNodes; - - - } - var startMediaIds = user.StartMediaIds.ToArray(); - if (startMediaIds.Length > 0) - { - var startNodes = new List(); - if (startContentIds.Contains(-1)) - { - startNodes.Add(RootNode(textService.Localize("media/mediaRoot"))); - } - var mediaItems = entityService.GetAll(UmbracoObjectTypes.Media, startMediaIds); - startNodes.AddRange(Mapper.Map, IEnumerable>(mediaItems)); - display.StartMediaIds = startNodes; - } - }); + .ForMember(dest => dest.AdditionalData, opt => opt.Ignore()); CreateMap() //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 GetStartNodeValues(int[] startNodeIds, + ILocalizedTextService textService, IEntityService entityService, UmbracoObjectTypes objectType, + string localizedKey) + { + if (startNodeIds.Length <= 0) + return Enumerable.Empty(); + + var startNodes = new List(); + if (startNodeIds.Contains(-1)) + startNodes.Add(RootNode(textService.Localize(localizedKey))); + + var mediaItems = entityService.GetAll(objectType, startNodeIds); + startNodes.AddRange(Mapper.Map, IEnumerable>(mediaItems)); + return startNodes; + } + private void MapUserGroupBasic(ISectionService sectionService, IEntityService entityService, ILocalizedTextService textService, dynamic group, UserGroupBasic display) { var allSections = sectionService.GetSections(); diff --git a/src/Umbraco.Web/Models/ProfileModel.cs b/src/Umbraco.Web/Models/ProfileModel.cs index d7910d2481..c999657b6a 100644 --- a/src/Umbraco.Web/Models/ProfileModel.cs +++ b/src/Umbraco.Web/Models/ProfileModel.cs @@ -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(); - 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; } diff --git a/src/Umbraco.Web/Models/RegisterModel.cs b/src/Umbraco.Web/Models/RegisterModel.cs index fcec6677b3..971cb8b916 100644 --- a/src/Umbraco.Web/Models/RegisterModel.cs +++ b/src/Umbraco.Web/Models/RegisterModel.cs @@ -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(); 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; } diff --git a/src/Umbraco.Web/PropertyEditors/TextAreaPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/TextAreaPropertyEditor.cs index c957a1da93..591691c5df 100644 --- a/src/Umbraco.Web/PropertyEditors/TextAreaPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/TextAreaPropertyEditor.cs @@ -10,8 +10,14 @@ namespace Umbraco.Web.PropertyEditors /// /// The constructor will setup the property editor based on the attribute if one is found /// - 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() { diff --git a/src/Umbraco.Web/PropertyEditors/TextOnlyValueEditor.cs b/src/Umbraco.Web/PropertyEditors/TextOnlyValueEditor.cs new file mode 100644 index 0000000000..7222ee13e4 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/TextOnlyValueEditor.cs @@ -0,0 +1,46 @@ +using System; +using Umbraco.Core.Models; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; + +namespace Umbraco.Web.PropertyEditors +{ + /// + /// 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 + /// + public class TextOnlyValueEditor : PropertyValueEditorWrapper + { + public TextOnlyValueEditor(PropertyValueEditor wrapped) : base(wrapped) + { + } + + /// + /// A method used to format the database value to a value that can be used by the editor + /// + /// + /// + /// + /// + /// + /// The object returned will always be a string and if the database type is not a valid string type an exception is thrown + /// + 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"); + } + } + + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/PropertyEditors/TextboxPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/TextboxPropertyEditor.cs index 4af57045b6..46bb037624 100644 --- a/src/Umbraco.Web/PropertyEditors/TextboxPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/TextboxPropertyEditor.cs @@ -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 /// /// The constructor will setup the property editor based on the attribute if one is found /// - 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() { diff --git a/src/Umbraco.Web/Routing/DomainHelper.cs b/src/Umbraco.Web/Routing/DomainHelper.cs index 38ccb02063..0b45407cab 100644 --- a/src/Umbraco.Web/Routing/DomainHelper.cs +++ b/src/Umbraco.Web/Routing/DomainHelper.cs @@ -240,7 +240,7 @@ namespace Umbraco.Web.Routing /// Eg the relative part of /foo/bar/nil to domain example.com/foo is /bar/nil. public static string PathRelativeToDomain(Uri domainUri, string path) { - return path.Substring(domainUri.AbsolutePath.Length).EnsureStartsWith('/'); + return path.Substring(domainUri.GetAbsolutePathDecoded().Length).EnsureStartsWith('/'); } #endregion diff --git a/src/Umbraco.Web/Routing/FacadeRouter.cs b/src/Umbraco.Web/Routing/FacadeRouter.cs index 0cc0601cf4..1016cf0d6f 100644 --- a/src/Umbraco.Web/Routing/FacadeRouter.cs +++ b/src/Umbraco.Web/Routing/FacadeRouter.cs @@ -61,7 +61,11 @@ namespace Umbraco.Web.Routing GetRolesForLogin = getRolesForLogin ?? (s => Roles.Provider.GetRolesForUser(s)); } - private Func> 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> GetRolesForLogin { get; } public PublishedContentRequest CreateRequest(UmbracoContext umbracoContext, Uri uri = null) { diff --git a/src/Umbraco.Web/Routing/RedirectTrackingComponent.cs b/src/Umbraco.Web/Routing/RedirectTrackingComponent.cs index 1baf69bb60..c9e867c23d 100644 --- a/src/Umbraco.Web/Routing/RedirectTrackingComponent.cs +++ b/src/Umbraco.Web/Routing/RedirectTrackingComponent.cs @@ -150,6 +150,7 @@ namespace Umbraco.Web.Redirects private static void ContentService_Moving(IContentService sender, MoveEventArgs e) { + //TODO: Use the new e.EventState to track state between Moving/Moved events! Moving = true; } diff --git a/src/Umbraco.Web/Scheduling/HealthCheckNotifier.cs b/src/Umbraco.Web/Scheduling/HealthCheckNotifier.cs index 394a0ca93d..397b3d35fb 100644 --- a/src/Umbraco.Web/Scheduling/HealthCheckNotifier.cs +++ b/src/Umbraco.Web/Scheduling/HealthCheckNotifier.cs @@ -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); } diff --git a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs index ae03c0914e..582e540499 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs @@ -26,6 +26,9 @@ namespace Umbraco.Web.Scheduling public override async Task PerformRunAsync(CancellationToken token) { + if (Suspendable.ScheduledPublishing.CanRun == false) + return true; // repeat, later + switch (_runtime.ServerRole) { case ServerRole.Slave: diff --git a/src/Umbraco.Web/Search/ExamineComponent.cs b/src/Umbraco.Web/Search/ExamineComponent.cs index 44710b4ba8..29e5d9b752 100644 --- a/src/Umbraco.Web/Search/ExamineComponent.cs +++ b/src/Umbraco.Web/Search/ExamineComponent.cs @@ -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(); diff --git a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs index 9020935b14..02a7208634 100644 --- a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs +++ b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs @@ -45,7 +45,8 @@ namespace Umbraco.Web.Security.Identity services.MemberTypeService, services.EntityService, services.ExternalLoginService, - userMembershipProvider)); + userMembershipProvider, + UmbracoConfig.For.UmbracoSettings().Content)); app.SetBackOfficeUserManagerType(); @@ -74,7 +75,8 @@ namespace Umbraco.Web.Security.Identity (options, owinContext) => BackOfficeUserManager.Create( options, customUserStore, - userMembershipProvider)); + userMembershipProvider, + UmbracoConfig.For.UmbracoSettings().Content)); app.SetBackOfficeUserManagerType(); diff --git a/src/Umbraco.Web/Security/MembershipHelper.cs b/src/Umbraco.Web/Security/MembershipHelper.cs index ec73f7831d..d1b47f9804 100644 --- a/src/Umbraco.Web/Security/MembershipHelper.cs +++ b/src/Umbraco.Web/Security/MembershipHelper.cs @@ -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 } } + /// + /// Check if a document object is protected by the "Protect Pages" functionality in umbraco + /// + /// The full path of the document object to check + /// True if the document object is protected + public virtual bool IsProtected(string path) + { + //this is a cached call + return PublicAccessService.IsProtected(path); + } + + /// + /// Check if the current user has access to a document + /// + /// The full path of the document object to check + /// True if the current user has access or if the current document isn't protected + public virtual bool MemberHasAccess(string path) + { + //cache this in the request cache + return ApplicationCache.RequestCache.GetCacheItem(string.Format("{0}.{1}-{2}", typeof(MembershipHelper), "MemberHasAccess", path), () => + { + if (IsProtected(path)) + { + return IsLoggedIn() && HasAccess(path, Roles.Provider); + } + return true; + }); + } + + /// + /// This will check if the member has access to this path + /// + /// + /// + /// + /// + /// 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. + /// + private bool HasAccess(string path, RoleProvider roleProvider) + { + return _umbracoContext.PublishedContentRequest == null + ? PublicAccessService.HasAccess(path, CurrentUserName, roleProvider.GetRolesForUser) + : PublicAccessService.HasAccess(path, CurrentUserName, Router.GetRolesForLogin); + } + /// /// Returns true if the current membership provider is the Umbraco built-in one. /// @@ -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 /// public virtual Attempt 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 /// private IMember GetCurrentPersistedMember() { - return _applicationCache.RequestCache.GetCacheItem( + return ApplicationCache.RequestCache.GetCacheItem( 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; }); } diff --git a/src/Umbraco.Web/Security/Providers/MembersMembershipProvider.cs b/src/Umbraco.Web/Security/Providers/MembersMembershipProvider.cs index e45f5e8d05..374920f99d 100644 --- a/src/Umbraco.Web/Security/Providers/MembersMembershipProvider.cs +++ b/src/Umbraco.Web/Security/Providers/MembersMembershipProvider.cs @@ -92,6 +92,13 @@ namespace Umbraco.Web.Security.Providers } } + protected override Attempt GetRawPassword(string username) + { + var found = MemberService.GetByUsername(username); + if (found == null) return Attempt.Fail(); + return Attempt.Succeed(found.RawPasswordValue); + } + public override string DefaultMemberTypeAlias { get diff --git a/src/Umbraco.Web/Security/Providers/UmbracoMembershipProvider.cs b/src/Umbraco.Web/Security/Providers/UmbracoMembershipProvider.cs index 02312318a2..ac494349a5 100644 --- a/src/Umbraco.Web/Security/Providers/UmbracoMembershipProvider.cs +++ b/src/Umbraco.Web/Security/Providers/UmbracoMembershipProvider.cs @@ -37,10 +37,10 @@ namespace Umbraco.Web.Security.Providers protected abstract MembershipUser ConvertToMembershipUser(TEntity entity); - private bool _allowManuallyChangingPassword = true; + private bool _allowManuallyChangingPassword = false; /// - /// 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 /// 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); } /// diff --git a/src/Umbraco.Web/Security/Providers/UsersMembershipProvider.cs b/src/Umbraco.Web/Security/Providers/UsersMembershipProvider.cs index 3c6d47be5d..440caaab9c 100644 --- a/src/Umbraco.Web/Security/Providers/UsersMembershipProvider.cs +++ b/src/Umbraco.Web/Security/Providers/UsersMembershipProvider.cs @@ -45,10 +45,36 @@ namespace Umbraco.Web.Security.Providers return entity.AsConcreteMembershipUser(Name, true); } + private bool _allowManuallyChangingPassword = false; + private bool _enablePasswordReset = false; + + /// + /// Indicates whether the membership provider is configured to allow users to reset their passwords. + /// + /// + /// true if the membership provider supports password reset; otherwise, false. The default is FALSE for users. + public override bool EnablePasswordReset + { + get { return _enablePasswordReset; } + } + + /// + /// For backwards compatibility, this provider supports this option by default it is FALSE for users + /// + 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 diff --git a/src/Umbraco.Web/Security/WebSecurity.cs b/src/Umbraco.Web/Security/WebSecurity.cs index eb71156e25..1da336c932 100644 --- a/src/Umbraco.Web/Security/WebSecurity.cs +++ b/src/Umbraco.Web/Security/WebSecurity.cs @@ -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 allowGroups = null, IEnumerable 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; - } - + } + /// /// Validates the current user to see if they have access to the specified app /// @@ -306,29 +307,28 @@ namespace Umbraco.Web.Security /// /// Checks if the specified user as access to the app /// - /// + /// /// /// - 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); } /// /// Checks if the specified user by username as access to the app /// - /// + /// /// /// - 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")] diff --git a/src/Umbraco.Web/Suspendable.cs b/src/Umbraco.Web/Suspendable.cs new file mode 100644 index 0000000000..877c9797e5 --- /dev/null +++ b/src/Umbraco.Web/Suspendable.cs @@ -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; + } + } + } +} diff --git a/src/Umbraco.Web/Trees/ApplicationTreeController.cs b/src/Umbraco.Web/Trees/ApplicationTreeController.cs index 5664214465..b5e5a6cfc0 100644 --- a/src/Umbraco.Web/Trees/ApplicationTreeController.cs +++ b/src/Umbraco.Web/Trees/ApplicationTreeController.cs @@ -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 diff --git a/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs b/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs index 89c55901bb..a0852c94f4 100644 --- a/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs @@ -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(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias))); menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionRefresh.Instance.Alias)), true); return menu; } diff --git a/src/Umbraco.Web/Trees/ContentTreeController.cs b/src/Umbraco.Web/Trees/ContentTreeController.cs index b0060c803b..96cdaae1e4 100644 --- a/src/Umbraco.Web/Trees/ContentTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentTreeController.cs @@ -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( + 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( + 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); } /// diff --git a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs index a0c22893b3..b0f76aba00 100644 --- a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs @@ -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 + /// + /// Ensure the noAccess metadata is applied for the root node if in dialog mode and the user doesn't have path access to it + /// + /// + /// + 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); + /// + /// Returns a for the and + /// attaches some meta data to the node if the user doesn't have start node access to it when in dialog mode + /// + /// + /// + /// + /// + 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; + } + /// /// Returns the /// @@ -68,13 +110,7 @@ namespace Umbraco.Web.Trees /// Returns the user's start node for this tree /// protected abstract int[] UserStartNodes { get; } - - /// - /// Gets the tree nodes for the given id - /// - /// - /// - /// + 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(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(); + //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 /// /// /// + //we should remove this in v8, it's now here for backwards compat only protected abstract bool HasPathAccess(string id, FormDataCollection queryStrings); + /// + /// Returns true or false if the current user has access to the node based on the user's allowed start node (path) access + /// + /// + /// + /// + protected bool HasPathAccess(IUmbracoEntity entity, FormDataCollection queryStrings) + { + if (entity == null) return false; + return Security.CurrentUser.HasPathAccess(entity, Services.EntityService, RecycleBinId); + } + /// /// Ensures the recycle bin is appended when required (i.e. user has access to the root and it's not in dialog mode) /// @@ -210,7 +292,7 @@ namespace Umbraco.Web.Trees /// 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(Services.TextService.Localize("actions", ActionRefresh.Instance.Alias), true); return menu; } + return PerformGetMenuForNode(id, queryStrings); } @@ -268,10 +351,11 @@ namespace Umbraco.Web.Trees internal IEnumerable 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 } + /// + /// this will parse the string into either a GUID or INT + /// + /// + /// + internal Tuple GetIdentifierFromString(string id) + { + Guid idGuid; + int idInt; + Udi idUdi; + + if (Guid.TryParse(id, out idGuid)) + { + return new Tuple(idGuid, null); + } + if (int.TryParse(id, out idInt)) + { + return new Tuple(null, idInt); + } + if (Udi.TryParse(id, out idUdi)) + { + var guidUdi = idUdi as GuidUdi; + if (guidUdi != null) + return new Tuple(guidUdi.Guid, null); + } + + return null; + } + /// /// Get an entity via an id that can be either an integer, Guid or UDI /// /// /// + /// + /// This object has it's own contextual cache for these lookups + /// 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 _entityCache = new ConcurrentDictionary(); } } diff --git a/src/Umbraco.Web/Trees/ContentTypeTreeController.cs b/src/Umbraco.Web/Trees/ContentTypeTreeController.cs index 96fe203919..6a61246db6 100644 --- a/src/Umbraco.Web/Trees/ContentTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentTypeTreeController.cs @@ -92,12 +92,19 @@ namespace Umbraco.Web.Trees menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias))); + menu.Items.Add(new MenuItem("rename", Services.TextService.Localize(String.Format("actions/{0}", "rename"))) + { + Icon = "icon icon-edit" + }); + if (container.HasChildren() == false) { //can delete doc type menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias)), true); } menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionRefresh.Instance.Alias)), true); + + } else { diff --git a/src/Umbraco.Web/Trees/DataTypeTreeController.cs b/src/Umbraco.Web/Trees/DataTypeTreeController.cs index 9d3f337152..abf6cd9e53 100644 --- a/src/Umbraco.Web/Trees/DataTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/DataTypeTreeController.cs @@ -100,6 +100,11 @@ namespace Umbraco.Web.Trees menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias))); + menu.Items.Add(new MenuItem("rename", Services.TextService.Localize(String.Format("actions/{0}", "rename"))) + { + Icon = "icon icon-edit" + }); + if (container.HasChildren() == false) { //can delete data type diff --git a/src/Umbraco.Web/Trees/LegacyTreeParams.cs b/src/Umbraco.Web/Trees/LegacyTreeParams.cs index d012551ad1..ddd822e9ef 100644 --- a/src/Umbraco.Web/Trees/LegacyTreeParams.cs +++ b/src/Umbraco.Web/Trees/LegacyTreeParams.cs @@ -15,13 +15,16 @@ namespace Umbraco.Web.Trees public LegacyTreeParams(IEnumerable> formCollection) { - var p = TreeRequestParams.FromDictionary(formCollection.ToDictionary(x => x.Key, x => x.Value)); - NodeKey = p.NodeKey; - StartNodeID = p.StartNodeID; - ShowContextMenu = p.ShowContextMenu; - IsDialog = p.IsDialog; - DialogMode = p.DialogMode; - FunctionToCall = p.FunctionToCall; + if (formCollection != null) + { + var p = TreeRequestParams.FromDictionary(formCollection.ToDictionary(x => x.Key, x => x.Value)); + NodeKey = p.NodeKey; + StartNodeID = p.StartNodeID; + ShowContextMenu = p.ShowContextMenu; + IsDialog = p.IsDialog; + DialogMode = p.DialogMode; + FunctionToCall = p.FunctionToCall; + } } public string NodeKey { get; set; } diff --git a/src/Umbraco.Web/Trees/MediaTreeController.cs b/src/Umbraco.Web/Trees/MediaTreeController.cs index 9c77122613..c6a40319c9 100644 --- a/src/Umbraco.Web/Trees/MediaTreeController.cs +++ b/src/Umbraco.Web/Trees/MediaTreeController.cs @@ -33,20 +33,7 @@ namespace Umbraco.Web.Trees public class MediaTreeController : 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.Media); - return node; - } - + protected override int RecycleBinId => Constants.System.RecycleBinMedia; protected override bool RecycleBinSmells => Services.MediaService.RecycleBinSmells(); @@ -96,9 +83,12 @@ namespace Umbraco.Web.Trees if (id == Constants.System.Root.ToInvariantString()) { - //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( + Services.TextService.Localize(string.Concat("actions/", ActionRefresh.Instance.Alias)), + true); return menu; } @@ -119,6 +109,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) + { + menu.Items.Add( + Services.TextService.Localize(string.Concat("actions/", ActionRefresh.Instance.Alias)), + true); + return menu; + } + //return a normal node menu: menu.Items.Add(Services.TextService.Localize("actions", ActionNew.Instance.Alias)); menu.Items.Add(Services.TextService.Localize("actions", ActionMove.Instance.Alias)); @@ -146,14 +146,8 @@ namespace Umbraco.Web.Trees protected override bool HasPathAccess(string id, FormDataCollection queryStrings) { var entity = GetEntityFromId(id); - if (entity == null) - return false; - var media = Services.MediaService.GetById(entity.Id); - if (media == null) - return false; - - return Security.CurrentUser.HasPathAccess(media, Services.EntityService); + return HasPathAccess(entity, queryStrings); } public IEnumerable Search(string query, int pageSize, long pageIndex, out long totalFound, string searchFrom = null) diff --git a/src/Umbraco.Web/Trees/MediaTypeTreeController.cs b/src/Umbraco.Web/Trees/MediaTypeTreeController.cs index 086f3437b9..75a953a5ef 100644 --- a/src/Umbraco.Web/Trees/MediaTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/MediaTypeTreeController.cs @@ -86,6 +86,11 @@ namespace Umbraco.Web.Trees menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias))); + menu.Items.Add(new MenuItem("rename", Services.TextService.Localize(String.Format("actions/{0}", "rename"))) + { + Icon = "icon icon-edit" + }); + if (container.HasChildren() == false) { //can delete doc type diff --git a/src/Umbraco.Web/Trees/TemplatesTreeController.cs b/src/Umbraco.Web/Trees/TemplatesTreeController.cs index 201ff7e70c..91bc7abf7f 100644 --- a/src/Umbraco.Web/Trees/TemplatesTreeController.cs +++ b/src/Umbraco.Web/Trees/TemplatesTreeController.cs @@ -87,10 +87,7 @@ namespace Umbraco.Web.Trees if (template.IsMasterTemplate == false) { //add delete option if it doesn't have children - menu.Items.Add(Services.TextService.Localize("actions", ActionDelete.Instance.Alias), true) - //Since we haven't implemented anything for languages in angular, this needs to be converted to - //use the legacy format - .ConvertLegacyMenuItem(entity, "templates", queryStrings.GetValue("application")); + menu.Items.Add(Services.TextService.Localize("actions", ActionDelete.Instance.Alias), true); } //add refresh diff --git a/src/Umbraco.Web/Trees/UserTreeController.cs b/src/Umbraco.Web/Trees/UserTreeController.cs index 0166663293..2027908657 100644 --- a/src/Umbraco.Web/Trees/UserTreeController.cs +++ b/src/Umbraco.Web/Trees/UserTreeController.cs @@ -7,7 +7,7 @@ using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.Trees { [UmbracoTreeAuthorize(Constants.Trees.Users)] - [Tree(Constants.Applications.Users, Constants.Trees.Users, null, sortOrder: 0)] + [Tree(Constants.Applications.Users, Constants.Trees.Users, "Users", sortOrder: 0)] [PluginController("UmbracoTrees")] [CoreTree] public class UserTreeController : TreeController diff --git a/src/Umbraco.Web/UI/Pages/UmbracoEnsuredPage.cs b/src/Umbraco.Web/UI/Pages/UmbracoEnsuredPage.cs index 55356abd69..f2c109bd6c 100644 --- a/src/Umbraco.Web/UI/Pages/UmbracoEnsuredPage.cs +++ b/src/Umbraco.Web/UI/Pages/UmbracoEnsuredPage.cs @@ -42,31 +42,31 @@ namespace Umbraco.Web.UI.Pages /// protected void CheckPathAndPermissions(int entityId, UmbracoObjectTypes objectType, IAction actionToCheck) { - if (objectType == UmbracoObjectTypes.Document || objectType == UmbracoObjectTypes.Media) + if (objectType != UmbracoObjectTypes.Document && objectType != UmbracoObjectTypes.Media) + return; + + //check path access + + var entity = entityId == Constants.System.Root + ? UmbracoEntity.Root + : Services.EntityService.Get(entityId, objectType); + var hasAccess = Security.CurrentUser.HasPathAccess(entity, Services.EntityService, objectType == UmbracoObjectTypes.Document ? Constants.System.RecycleBinContent : Constants.System.RecycleBinMedia); + if (hasAccess == false) + throw new AuthorizationException($"The current user doesn't have access to the path '{entity.Path}'"); + + //only documents have action permissions + if (objectType == UmbracoObjectTypes.Document) { - //check path access - - var entity = entityId == Constants.System.Root - ? UmbracoEntity.Root - : Services.EntityService.Get(entityId, objectType); - var hasAccess = Security.CurrentUser.HasPathAccess(entity, Services.EntityService, objectType == UmbracoObjectTypes.Document ? Constants.System.RecycleBinContent : Constants.System.RecycleBinMedia); - if (hasAccess == false) - throw new AuthorizationException($"The current user doesn't have access to the path '{entity.Path}'"); - - //only documents have action permissions - if (objectType == UmbracoObjectTypes.Document) - { - var allActions = Current.Actions; - var perms = Security.CurrentUser.GetPermissions(entity.Path, Services.UserService); - var actions = perms - .Select(x => allActions.FirstOrDefault(y => y.Letter.ToString(CultureInfo.InvariantCulture) == x)) - .WhereNotNull(); - if (actions.Contains(actionToCheck) == false) - throw new AuthorizationException($"The current user doesn't have permission to {actionToCheck.Alias} on the path '{entity.Path}'"); - } + var allActions = Current.Actions; + var perms = Security.CurrentUser.GetPermissions(entity.Path, Services.UserService); + var actions = perms + .Select(x => allActions.FirstOrDefault(y => y.Letter.ToString(CultureInfo.InvariantCulture) == x)) + .WhereNotNull(); + if (actions.Contains(actionToCheck) == false) + throw new AuthorizationException($"The current user doesn't have permission to {actionToCheck.Alias} on the path '{entity.Path}'"); } } - + private bool _hasValidated = false; /// diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 0e1de88d8f..b22111e775 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -127,6 +127,7 @@ + @@ -134,6 +135,9 @@ + + + @@ -244,6 +248,7 @@ + @@ -323,6 +328,7 @@ + @@ -340,10 +346,13 @@ ASPXCodeBehind + + + diff --git a/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs b/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs index a8c08fcaa3..f2675c21ae 100644 --- a/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs +++ b/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs @@ -44,12 +44,7 @@ namespace Umbraco.Web protected virtual void ConfigureServices(IAppBuilder app, ServiceContext services) { app.SetUmbracoLoggerFactory(); - - //Configure the Identity user manager for use with Umbraco Back office - // (EXPERT: an overload accepts a custom BackOfficeUserStore implementation) - app.ConfigureUserManagerForUmbracoBackOffice( - services, - Core.Security.MembershipProviderExtensions.GetUsersMembershipProvider().AsUmbracoMembershipProvider()); + ConfigureUmbracoUserManager(app); } /// @@ -68,6 +63,18 @@ namespace Umbraco.Web .FinalizeMiddlewareConfiguration(); } + /// + /// Configure the Identity user manager for use with Umbraco Back office + /// + /// + protected virtual void ConfigureUmbracoUserManager(IAppBuilder app) + { + // (EXPERT: an overload accepts a custom BackOfficeUserStore implementation) + app.ConfigureUserManagerForUmbracoBackOffice( + Current.Services, + Core.Security.MembershipProviderExtensions.GetUsersMembershipProvider().AsUmbracoMembershipProvider()); + } + public static event EventHandler MiddlewareConfigured; internal static void OnMiddlewareConfigured(OwinMiddlewareConfiguredEventArgs args) diff --git a/src/Umbraco.Web/UmbracoHelper.cs b/src/Umbraco.Web/UmbracoHelper.cs index a3eaa39abd..2fb7b46aa7 100644 --- a/src/Umbraco.Web/UmbracoHelper.cs +++ b/src/Umbraco.Web/UmbracoHelper.cs @@ -300,7 +300,7 @@ namespace Umbraco.Web /// True if the document object is protected public bool IsProtected(string path) { - return _services.PublicAccessService.IsProtected(path); + return MembershipHelper.IsProtected(path); } [EditorBrowsable(EditorBrowsableState.Never)] @@ -317,24 +317,7 @@ namespace Umbraco.Web /// True if the current user has access or if the current document isn't protected public bool MemberHasAccess(string path) { - if (IsProtected(path)) - { - return MembershipHelper.IsLoggedIn() - && _services.PublicAccessService.HasAccess(path, GetCurrentMember(), Roles.Provider); - } - return true; - } - - /// - /// Gets (or adds) the current member from the current request cache - /// - private MembershipUser GetCurrentMember() - { - return _appCache.RequestCache.GetCacheItem("UmbracoHelper.GetCurrentMember", () => - { - var provider = Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider(); - return provider.GetCurrentUser(); - }); + return MembershipHelper.MemberHasAccess(path); } /// @@ -450,7 +433,7 @@ namespace Umbraco.Web { return MembershipHelper.GetByProviderKey(id); } - + public IPublishedContent Member(object id) { var asInt = id.TryConvertTo(); @@ -710,7 +693,7 @@ namespace Umbraco.Web } guidId = null; return false; - } + } #endregion @@ -730,7 +713,7 @@ namespace Umbraco.Web var entityService = Current.Services.EntityService; // fixme inject var mediaAttempt = entityService.GetIdForKey(id, UmbracoObjectTypes.Media); return mediaAttempt.Success ? ContentQuery.Media(mediaAttempt.Result) : null; - } + } /// /// Overloaded method accepting an 'object' type @@ -913,7 +896,7 @@ namespace Umbraco.Web public string CreateHash(string text) { return text.GenerateHash(); - } + } /// /// Strips all html tags from a given string, all contents of the tags will remain. @@ -1002,8 +985,51 @@ namespace Umbraco.Web { return StringUtilities.Truncate(html, length, addElipsis, treatTagsAsContent); } + + #region Truncate by Words + /// + /// Truncates a string to a given amount of words, can add a elipsis at the end (...). Method checks for open html tags, and makes sure to close them + /// + public IHtmlString TruncateByWords(string html, int words) + { + int length = StringUtilities.WordsToLength(html, words); + return Truncate(html, length, true, false); + } + + /// + /// Truncates a string to a given amount of words, can add a elipsis at the end (...). Method checks for open html tags, and makes sure to close them + /// + public IHtmlString TruncateByWords(string html, int words, bool addElipsis) + { + int length = StringUtilities.WordsToLength(html, words); + + return Truncate(html, length, addElipsis, false); + } + + /// + /// Truncates a string to a given amount of words, can add a elipsis at the end (...). Method checks for open html tags, and makes sure to close them + /// + public IHtmlString TruncateByWords(IHtmlString html, int words) + { + int length = StringUtilities.WordsToLength(html.ToHtmlString(), words); + + return Truncate(html, length, true, false); + } + + /// + /// Truncates a string to a given amount of words, can add a elipsis at the end (...). Method checks for open html tags, and makes sure to close them + /// + public IHtmlString TruncateByWords(IHtmlString html, int words, bool addElipsis) + { + int length = StringUtilities.WordsToLength(html.ToHtmlString(), words); + + return Truncate(html, length, addElipsis, false); + } + + #endregion + #endregion #region If diff --git a/src/Umbraco.Web/WebApi/EnableDetailedErrorsAttribute.cs b/src/Umbraco.Web/WebApi/EnableDetailedErrorsAttribute.cs new file mode 100644 index 0000000000..1d35d19134 --- /dev/null +++ b/src/Umbraco.Web/WebApi/EnableDetailedErrorsAttribute.cs @@ -0,0 +1,17 @@ +using System.Web.Http; +using System.Web.Http.Controllers; +using System.Web.Http.Filters; + +namespace Umbraco.Web.WebApi +{ + /// + /// Ensures controllers have detailed error messages even when debug mode is off + /// + public class EnableDetailedErrorsAttribute : ActionFilterAttribute + { + public override void OnActionExecuting(HttpActionContext actionContext) + { + actionContext.ControllerContext.Configuration.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/WebApi/Filters/AngularAntiForgeryHelper.cs b/src/Umbraco.Web/WebApi/Filters/AngularAntiForgeryHelper.cs index a39e16c687..6eed3c095d 100644 --- a/src/Umbraco.Web/WebApi/Filters/AngularAntiForgeryHelper.cs +++ b/src/Umbraco.Web/WebApi/Filters/AngularAntiForgeryHelper.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; -using System.Collections.Specialized; using System.Linq; -using System.Net.Http; using System.Net.Http.Headers; using System.Web.Helpers; using Umbraco.Core; @@ -19,19 +17,17 @@ namespace Umbraco.Web.WebApi.Filters /// /// The cookie name that is used to store the validation value /// - public const string CsrfValidationCookieName = "XSRF-V"; + public const string CsrfValidationCookieName = "UMB-XSRF-V"; /// - /// The cookie name that is set for angular to use to pass in to the header value for "X-XSRF-TOKEN" + /// The cookie name that is set for angular to use to pass in to the header value for "X-UMB-XSRF-TOKEN" /// - public const string AngularCookieName = "XSRF-TOKEN"; + public const string AngularCookieName = "UMB-XSRF-TOKEN"; /// /// The header name that angular uses to pass in the token to validate the cookie /// - public const string AngularHeadername = "X-XSRF-TOKEN"; - - + public const string AngularHeadername = "X-UMB-XSRF-TOKEN"; /// /// Returns 2 tokens - one for the cookie value and one that angular should set as the header value @@ -112,15 +108,13 @@ namespace Umbraco.Web.WebApi.Filters /// public static bool ValidateHeaders(HttpRequestHeaders requestHeaders, out string failedReason) { - var cookieToken = requestHeaders - .GetCookies() - .Select(c => c[CsrfValidationCookieName]) - .FirstOrDefault(); + var cookieToken = requestHeaders.GetCookieValue(CsrfValidationCookieName); return ValidateHeaders( requestHeaders.ToDictionary(x => x.Key, x => x.Value).ToArray(), - cookieToken == null ? null : cookieToken.Value, + cookieToken == null ? null : cookieToken, out failedReason); } + } } diff --git a/src/Umbraco.Web/WebApi/Filters/LegacyTreeAuthorizeAttribute.cs b/src/Umbraco.Web/WebApi/Filters/LegacyTreeAuthorizeAttribute.cs index 46bfe73517..5d1b90a09d 100644 --- a/src/Umbraco.Web/WebApi/Filters/LegacyTreeAuthorizeAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/LegacyTreeAuthorizeAttribute.cs @@ -19,7 +19,7 @@ namespace Umbraco.Web.WebApi.Filters if (tree == null) return false; return UmbracoContext.Current.Security.CurrentUser != null - && UmbracoContext.Current.Security.UserHasAppAccess(tree.ApplicationAlias, UmbracoContext.Current.Security.CurrentUser); + && UmbracoContext.Current.Security.UserHasSectionAccess(tree.ApplicationAlias, UmbracoContext.Current.Security.CurrentUser); } return false; diff --git a/src/Umbraco.Web/WebApi/Filters/UmbracoApplicationAuthorizeAttribute.cs b/src/Umbraco.Web/WebApi/Filters/UmbracoApplicationAuthorizeAttribute.cs index 5dbd36d630..a479d8bce8 100644 --- a/src/Umbraco.Web/WebApi/Filters/UmbracoApplicationAuthorizeAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/UmbracoApplicationAuthorizeAttribute.cs @@ -36,7 +36,7 @@ namespace Umbraco.Web.WebApi.Filters } var authorized = Current.UmbracoContext.Security.CurrentUser != null - && _appNames.Any(app => Current.UmbracoContext.Security.UserHasAppAccess( + && _appNames.Any(app => Current.UmbracoContext.Security.UserHasSectionAccess( app, Current.UmbracoContext.Security.CurrentUser)); return authorized; diff --git a/src/Umbraco.Web/WebApi/Filters/UmbracoTreeAuthorizeAttribute.cs b/src/Umbraco.Web/WebApi/Filters/UmbracoTreeAuthorizeAttribute.cs index a36b2a14f6..7318c9d9b8 100644 --- a/src/Umbraco.Web/WebApi/Filters/UmbracoTreeAuthorizeAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/UmbracoTreeAuthorizeAttribute.cs @@ -48,7 +48,7 @@ namespace Umbraco.Web.WebApi.Filters .ToArray(); return Current.UmbracoContext.Security.CurrentUser != null - && apps.Any(app => Current.UmbracoContext.Security.UserHasAppAccess( + && apps.Any(app => Current.UmbracoContext.Security.UserHasSectionAccess( app, Current.UmbracoContext.Security.CurrentUser)); } } diff --git a/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs b/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs index 4f5d7cfb5c..0b43f41120 100644 --- a/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs +++ b/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs @@ -17,12 +17,14 @@ namespace Umbraco.Web.WebApi [DisableBrowserCache] [UmbracoWebApiRequireHttps] [CheckIfUserTicketDataIsStale] + [UnhandedExceptionLoggerConfiguration] + [EnableDetailedErrors] public abstract class UmbracoAuthorizedApiController : UmbracoApiController { private BackOfficeUserManager _userManager; private bool _userisValidated = false; - protected BackOfficeUserManager UserManager + protected BackOfficeUserManager UserManager => _userManager ?? (_userManager = TryGetOwinContext().Result.GetBackOfficeUserManager()); /// diff --git a/src/Umbraco.Web/WebApi/UnhandedExceptionLoggerConfigurationAttribute.cs b/src/Umbraco.Web/WebApi/UnhandedExceptionLoggerConfigurationAttribute.cs new file mode 100644 index 0000000000..51c43b0821 --- /dev/null +++ b/src/Umbraco.Web/WebApi/UnhandedExceptionLoggerConfigurationAttribute.cs @@ -0,0 +1,29 @@ +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Web.Http.Controllers; +using System.Web.Http.ExceptionHandling; +using System.Web.Http.Filters; +using Umbraco.Core; +using Umbraco.Core.Logging; + +namespace Umbraco.Web.WebApi +{ + /// + /// Adds our unhandled exception logger to the controller's services + /// + /// + /// Important to note that the will only be called if the controller has an ExceptionFilter applied + /// to it, so to kill two birds with one stone, this class inherits from ExceptionFilterAttribute purely to force webapi to use the + /// IExceptionLogger (strange) + /// + public class UnhandedExceptionLoggerConfigurationAttribute : ExceptionFilterAttribute, IControllerConfiguration + { + public virtual void Initialize(HttpControllerSettings controllerSettings, HttpControllerDescriptor controllerDescriptor) + { + controllerSettings.Services.Add(typeof(IExceptionLogger), new UnhandledExceptionLogger()); + } + + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/WebApi/UnhandledExceptionLogger.cs b/src/Umbraco.Web/WebApi/UnhandledExceptionLogger.cs new file mode 100644 index 0000000000..2bb0e33212 --- /dev/null +++ b/src/Umbraco.Web/WebApi/UnhandledExceptionLogger.cs @@ -0,0 +1,37 @@ +using System.Web.Http.ExceptionHandling; +using Umbraco.Core; +using Umbraco.Core.Composing; +using Umbraco.Core.Logging; + +namespace Umbraco.Web.WebApi +{ + /// + /// Used to log unhandled exceptions in webapi controllers + /// + public class UnhandledExceptionLogger : ExceptionLogger + { + private readonly ILogger _logger; + + public UnhandledExceptionLogger() + : this(Current.Logger) + { + } + + public UnhandledExceptionLogger(ILogger logger) + { + _logger = logger; + } + + public override void Log(ExceptionLoggerContext context) + { + if (context != null && context.ExceptionContext != null + && context.ExceptionContext.ActionContext != null && context.ExceptionContext.ActionContext.ControllerContext != null + && context.ExceptionContext.ActionContext.ControllerContext.Controller != null + && context.Exception != null) + { + _logger.Error(context.ExceptionContext.ActionContext.ControllerContext.Controller.GetType(), "Unhandled controller exception occurred", context.Exception); + } + } + + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/WebServices/UmbracoAuthorizedHttpHandler.cs b/src/Umbraco.Web/WebServices/UmbracoAuthorizedHttpHandler.cs index d88ec06dd1..248eea3d93 100644 --- a/src/Umbraco.Web/WebServices/UmbracoAuthorizedHttpHandler.cs +++ b/src/Umbraco.Web/WebServices/UmbracoAuthorizedHttpHandler.cs @@ -71,7 +71,7 @@ namespace Umbraco.Web.WebServices /// protected bool UserHasAppAccess(string app, IUser user) { - return Security.UserHasAppAccess(app, user); + return Security.UserHasSectionAccess(app, user); } /// @@ -82,7 +82,7 @@ namespace Umbraco.Web.WebServices /// protected bool UserHasAppAccess(string app, string username) { - return Security.UserHasAppAccess(app, username); + return Security.UserHasSectionAccess(app, username); } /// diff --git a/src/Umbraco.Web/WebServices/UmbracoAuthorizedWebService.cs b/src/Umbraco.Web/WebServices/UmbracoAuthorizedWebService.cs index 4cac946491..0a47ce0c8b 100644 --- a/src/Umbraco.Web/WebServices/UmbracoAuthorizedWebService.cs +++ b/src/Umbraco.Web/WebServices/UmbracoAuthorizedWebService.cs @@ -73,7 +73,7 @@ namespace Umbraco.Web.WebServices /// protected bool UserHasAppAccess(string app, IUser user) { - return Security.UserHasAppAccess(app, user); + return Security.UserHasSectionAccess(app, user); } /// @@ -84,7 +84,7 @@ namespace Umbraco.Web.WebServices /// protected bool UserHasAppAccess(string app, string username) { - return Security.UserHasAppAccess(app, username); + return Security.UserHasSectionAccess(app, username); } /// diff --git a/src/Umbraco.Web/_Legacy/UI/LegacyDialogHandler.cs b/src/Umbraco.Web/_Legacy/UI/LegacyDialogHandler.cs index bb37b30e86..07ed6c03d7 100644 --- a/src/Umbraco.Web/_Legacy/UI/LegacyDialogHandler.cs +++ b/src/Umbraco.Web/_Legacy/UI/LegacyDialogHandler.cs @@ -43,9 +43,9 @@ namespace Umbraco.Web._Legacy.UI /// private static ITask GetTaskForOperation(HttpContextBase httpContext, IUser umbracoUser, Operation op, string nodeType) { - if (httpContext == null) throw new ArgumentNullException("httpContext"); - if (umbracoUser == null) throw new ArgumentNullException("umbracoUser"); - if (nodeType == null) throw new ArgumentNullException("nodeType"); + if (httpContext == null) throw new ArgumentNullException(nameof(httpContext)); + if (umbracoUser == null) throw new ArgumentNullException(nameof(umbracoUser)); + if (nodeType == null) throw new ArgumentNullException(nameof(nodeType)); var ctxKey = op == Operation.Create ? ContextKeyCreate : ContextKeyDelete; @@ -120,17 +120,37 @@ namespace Umbraco.Web._Legacy.UI internal static bool UserHasCreateAccess(HttpContextBase httpContext, IUser umbracoUser, string nodeType) { var task = GetTaskForOperation(httpContext, umbracoUser, Operation.Create, nodeType); - var dialogTask = task as LegacyDialogTask; - if (dialogTask != null) - { - return dialogTask.ValidateUserForApplication(); - } - return true; + if (task == null) + throw new InvalidOperationException($"Could not task for operation {Operation.Create} for node type {nodeType}."); + return task is LegacyDialogTask ltask ? ltask.ValidateUserForApplication() : true; + } + + /// + /// Checks if the user has access to launch the ITask that matches the node type based on the app assigned + /// + /// + /// + /// + /// + /// + /// If the ITask doesn't implement LegacyDialogTask then we will return 'true' since we cannot validate + /// the application assigned. + /// + /// TODO: Create an API to assign a nodeType to an app so developers can manually secure it + /// + internal static bool UserHasDeleteAccess(HttpContextBase httpContext, User umbracoUser, string nodeType) + { + var task = GetTaskForOperation(httpContext, umbracoUser, Operation.Delete, nodeType); + if (task == null) + throw new InvalidOperationException($"Could not task for operation {Operation.Delete} for node type {nodeType}"); + return task is LegacyDialogTask ltask ? ltask.ValidateUserForApplication() : true; } public static void Delete(HttpContextBase httpContext, IUser umbracoUser, string nodeType, int nodeId, string text) { var typeInstance = GetTaskForOperation(httpContext, umbracoUser, Operation.Delete, nodeType); + if (typeInstance == null) + throw new InvalidOperationException($"Could not task for operation {Operation.Delete} for node type {nodeType}"); typeInstance.ParentID = nodeId; typeInstance.Alias = text; @@ -141,6 +161,8 @@ namespace Umbraco.Web._Legacy.UI public static string Create(HttpContextBase httpContext, IUser umbracoUser, string nodeType, int nodeId, string text, int typeId = 0) { var typeInstance = GetTaskForOperation(httpContext, umbracoUser, Operation.Create, nodeType); + if (typeInstance == null) + throw new InvalidOperationException($"Could not task for operation {Operation.Create} for node type {nodeType}"); typeInstance.TypeID = typeId; typeInstance.ParentID = nodeId; @@ -158,6 +180,8 @@ namespace Umbraco.Web._Legacy.UI internal static string Create(HttpContextBase httpContext, IUser umbracoUser, string nodeType, int nodeId, string text, IDictionary additionalValues, int typeId = 0) { var typeInstance = GetTaskForOperation(httpContext, umbracoUser, Operation.Create, nodeType); + if (typeInstance == null) + throw new InvalidOperationException($"Could not task for operation {Operation.Create} for node type {nodeType}"); typeInstance.TypeID = typeId; typeInstance.ParentID = nodeId; diff --git a/src/Umbraco.Web/umbraco.presentation/library.cs b/src/Umbraco.Web/umbraco.presentation/library.cs index d6714db3a9..7e856b67c4 100644 --- a/src/Umbraco.Web/umbraco.presentation/library.cs +++ b/src/Umbraco.Web/umbraco.presentation/library.cs @@ -1317,6 +1317,7 @@ namespace umbraco { try { + var mailSender = new EmailSender(); using (var mail = new MailMessage()) { mail.From = new MailAddress(fromMail.Trim()); @@ -1325,8 +1326,7 @@ namespace umbraco mail.Subject = subject; mail.IsBodyHtml = isHtml; mail.Body = body; - using (var smtpClient = new SmtpClient()) - smtpClient.Send(mail); + mailSender.Send(mail); } } catch (Exception ee) diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/AssignDomain2.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/AssignDomain2.aspx.cs index 9eb763bfd6..ea1ff391e5 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/AssignDomain2.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/AssignDomain2.aspx.cs @@ -2,6 +2,7 @@ using System.Text; using System.Linq; using Umbraco.Core; +using Umbraco.Core.Models; using Umbraco.Core.Services; using Umbraco.Web.UI.Pages; using Umbraco.Web; @@ -14,6 +15,14 @@ namespace umbraco.dialogs { public partial class AssignDomain2 : UmbracoEnsuredPage { + protected override void OnInit(EventArgs e) + { + base.OnInit(e); + + var nodeId = GetNodeId(); + CheckPathAndPermissions(nodeId, UmbracoObjectTypes.Document, ActionAssignDomain.Instance); + } + protected override void OnLoad(EventArgs e) { base.OnLoad(e); @@ -28,17 +37,7 @@ namespace umbraco.dialogs pane_domains.Visible = false; p_buttons.Visible = false; return; - } - - var permissions = Services.UserService.GetPermissions(Security.CurrentUser, node.Path); - if (permissions.AssignedPermissions.Contains(ActionAssignDomain.Instance.Letter.ToString(), StringComparer.Ordinal) == false) - { - feedback.Text = Services.TextService.Localize("assignDomain/permissionDenied"); - pane_language.Visible = false; - pane_domains.Visible = false; - p_buttons.Visible = false; - return; - } + } pane_language.Title = Services.TextService.Localize("assignDomain/setLanguage"); pane_domains.Title = Services.TextService.Localize("assignDomain/setDomains"); diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/sendToTranslation.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/sendToTranslation.aspx.cs index 0bd1374fb2..a60ea845e9 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/sendToTranslation.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/sendToTranslation.aspx.cs @@ -14,6 +14,7 @@ using Umbraco.Core.Models.Membership; using Umbraco.Web; using Umbraco.Web.Composing; using Umbraco.Web.UI.Pages; +using Umbraco.Core; namespace umbraco.presentation.dialogs { diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/sort.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/sort.aspx.cs index 0ac86233d6..d329147ddb 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/sort.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/sort.aspx.cs @@ -7,6 +7,9 @@ using System.Web.UI; using System.Collections.Generic; using Umbraco.Web.Composing; using Umbraco.Web.UI.Pages; +using Umbraco.Core.Exceptions; +using Umbraco.Core.Models; +using Umbraco.Web._Legacy.Actions; namespace umbraco.cms.presentation { @@ -15,6 +18,12 @@ namespace umbraco.cms.presentation /// public partial class sort : UmbracoEnsuredPage { + /// + /// The Parent Id being sorted + /// + protected int? ParentIdAsInt { get; private set; } + protected string ParentIdAsString { get; private set; } + private readonly List _nodes = new List(); protected bool HideDateColumn @@ -27,6 +36,21 @@ namespace umbraco.cms.presentation { CurrentApp = Request.GetItemAsString("app"); + ParentIdAsString = Request.GetItemAsString("ID"); + int parentId; + if (int.TryParse(ParentIdAsString, out parentId)) + { + ParentIdAsInt = parentId; + + if (CurrentApp == Constants.Applications.Content || CurrentApp == Constants.Applications.Media) + { + CheckPathAndPermissions( + ParentIdAsInt.Value, + CurrentApp == Constants.Applications.Content ? UmbracoObjectTypes.Document : UmbracoObjectTypes.Media, + ActionSort.Instance); + } + } + base.OnInit(e); } @@ -44,23 +68,22 @@ namespace umbraco.cms.presentation var app = Request.GetItemAsString("app"); var icon = "../images/umbraco/doc.gif"; - - int parentId; - if (int.TryParse(Request.GetItemAsString("ID"), out parentId)) + + if (ParentIdAsInt.HasValue) { if (app == Constants.Applications.Media) { icon = "../images/umbraco/mediaPhoto.gif"; var mediaService = Current.Services.MediaService; - if (parentId == -1) + if (ParentIdAsInt.Value == -1) { foreach (var child in mediaService.GetRootMedia().ToList().OrderBy(x => x.SortOrder)) _nodes.Add(CreateNode(child.Id.ToInvariantString(), child.SortOrder, child.Name, child.CreateDate, icon)); } else { - var children = mediaService.GetChildren(parentId); + var children = mediaService.GetChildren(ParentIdAsInt.Value); foreach (var child in children.OrderBy(x => x.SortOrder)) _nodes.Add(CreateNode(child.Id.ToInvariantString(), child.SortOrder, child.Name, child.CreateDate, icon)); } @@ -70,14 +93,14 @@ namespace umbraco.cms.presentation { var contentService = Current.Services.ContentService; - if (parentId == -1) + if (ParentIdAsInt.Value == -1) { foreach (var child in contentService.GetRootContent().ToList().OrderBy(x => x.SortOrder)) _nodes.Add(CreateNode(child.Id.ToInvariantString(), child.SortOrder, child.Name, child.CreateDate, icon)); } else { - var children = contentService.GetChildren(parentId); + var children = contentService.GetChildren(ParentIdAsInt.Value); foreach (var child in children) _nodes.Add(CreateNode(child.Id.ToInvariantString(), child.SortOrder, child.Name, child.CreateDate, icon)); } @@ -94,7 +117,7 @@ namespace umbraco.cms.presentation HideDateColumn = true; - var stylesheetName = Request.GetItemAsString("ID"); + var stylesheetName = ParentIdAsString; if (stylesheetName.IsNullOrWhiteSpace())throw new NullReferenceException("No Id passed in to editor"); var stylesheet = Services.FileService.GetStylesheetByName(stylesheetName.EnsureEndsWith(".css")); if (stylesheet == null) throw new InvalidOperationException("No stylesheet found by name " + stylesheetName);