diff --git a/src/Umbraco.Core/Configuration/UmbracoVersion.cs b/src/Umbraco.Core/Configuration/UmbracoVersion.cs index c2ca568a54..a5fe8de270 100644 --- a/src/Umbraco.Core/Configuration/UmbracoVersion.cs +++ b/src/Umbraco.Core/Configuration/UmbracoVersion.cs @@ -1,4 +1,5 @@ using System; +using System.Configuration; using System.Reflection; using Semver; @@ -34,11 +35,37 @@ namespace Umbraco.Core.Configuration /// /// Gets the semantic version of the executing code. /// - public static SemVersion SemanticVersion => new SemVersion( - Current.Major, - Current.Minor, - Current.Build, - CurrentComment.IsNullOrWhiteSpace() ? null : CurrentComment, - Current.Revision > 0 ? Current.Revision.ToInvariantString() : null); + public static SemVersion SemanticVersion { get; } = new SemVersion( + Current.Major, + Current.Minor, + Current.Build, + CurrentComment.IsNullOrWhiteSpace() ? null : CurrentComment, + Current.Revision > 0 ? Current.Revision.ToInvariantString() : null); + + /// + /// Gets the "local" version of the site. + /// + /// + /// Three things have a version, really: the executing code, the database model, + /// and the site/files. The database model version is entirely managed via migrations, + /// and changes during an upgrade. The executing code version changes when new code is + /// deployed. The site/files version changes during an upgrade. + /// + public static SemVersion Local + { + get + { + try + { + // fixme - this should live in its own independent file! NOT web.config! + var value = ConfigurationManager.AppSettings["umbracoConfigurationStatus"]; + return SemVersion.TryParse(value, out var semver) ? semver : null; + } + catch + { + return null; + } + } + } } } diff --git a/src/Umbraco.Core/Runtime/CoreRuntime.cs b/src/Umbraco.Core/Runtime/CoreRuntime.cs index 41e2c771b7..7d28ffcf57 100644 --- a/src/Umbraco.Core/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Core/Runtime/CoreRuntime.cs @@ -235,14 +235,14 @@ namespace Umbraco.Core.Runtime private void SetRuntimeStateLevel(RuntimeState runtimeState, IUmbracoDatabaseFactory databaseFactory, ILogger logger) { - var localVersion = LocalVersion; // the local, files, version + var localVersion = UmbracoVersion.Local; // the local, files, version var codeVersion = runtimeState.SemanticVersion; // the executing code version var connect = false; // we don't know yet runtimeState.Level = RuntimeLevel.Unknown; - if (string.IsNullOrWhiteSpace(localVersion)) + if (localVersion == null) { // there is no local version, we are not installed logger.Debug("No local version, need to install Umbraco."); @@ -350,22 +350,6 @@ namespace Umbraco.Core.Runtime return state == finalState; } - private static string LocalVersion - { - get - { - try - { - // fixme - this should live in its own independent file! NOT web.config! - return ConfigurationManager.AppSettings["umbracoConfigurationStatus"]; - } - catch - { - return string.Empty; - } - } - } - #region Locals protected ILogger Logger { get; private set; } diff --git a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs index cf545d0687..2bdad031de 100644 --- a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs +++ b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs @@ -43,7 +43,7 @@ namespace Umbraco.Core.Sync private bool _syncing; private bool _released; - protected DatabaseServerMessengerOptions Options { get; } + public DatabaseServerMessengerOptions Options { get; } public DatabaseServerMessenger( IRuntimeState runtime, IScopeProvider scopeProvider, ISqlContext sqlContext, ILogger logger, ProfilingLogger proflog, @@ -86,8 +86,7 @@ namespace Umbraco.Core.Sync { var idsA = ids?.ToArray(); - Type idType; - if (GetArrayType(idsA, out idType) == false) + if (GetArrayType(idsA, out var idType) == false) throw new ArgumentException("All items must be of the same type, either int or Guid.", nameof(ids)); var instructions = RefreshInstruction.GetInstructions(refresher, messageType, idsA, idType, json); diff --git a/src/Umbraco.Core/Sync/DatabaseServerRegistrar.cs b/src/Umbraco.Core/Sync/DatabaseServerRegistrar.cs index 09b6b72478..b6ca5912e2 100644 --- a/src/Umbraco.Core/Sync/DatabaseServerRegistrar.cs +++ b/src/Umbraco.Core/Sync/DatabaseServerRegistrar.cs @@ -14,7 +14,7 @@ namespace Umbraco.Core.Sync /// /// Gets or sets the registrar options. /// - public DatabaseServerRegistrarOptions Options { get; private set; } + public DatabaseServerRegistrarOptions Options { get; } /// /// Initializes a new instance of the class. @@ -23,20 +23,14 @@ namespace Umbraco.Core.Sync /// Some options. public DatabaseServerRegistrar(Lazy registrationService, DatabaseServerRegistrarOptions options) { - if (registrationService == null) throw new ArgumentNullException("registrationService"); - if (options == null) throw new ArgumentNullException("options"); - - Options = options; - _registrationService = registrationService; + Options = options ?? throw new ArgumentNullException(nameof(options)); + _registrationService = registrationService ?? throw new ArgumentNullException(nameof(registrationService)); } /// /// Gets the registered servers. /// - public IEnumerable Registrations - { - get { return _registrationService.Value.GetActiveServers(); } - } + public IEnumerable Registrations => _registrationService.Value.GetActiveServers(); /// /// Gets the role of the current server in the application environment. diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 6a19b00f22..e4a2d0c9bb 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -59,7 +59,7 @@ - 1.9.2 + 1.9.6 diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 4c8df3cc87..650ed00f0b 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -54,6 +54,9 @@ ..\packages\AutoMapper.6.1.1\lib\net45\AutoMapper.dll + + ..\packages\ClientDependency.1.9.6\lib\net45\ClientDependency.Core.dll + ..\packages\ImageProcessor.Web.4.8.4\lib\net45\ImageProcessor.Web.dll @@ -254,10 +257,6 @@ - - ..\packages\ClientDependency.1.9.2\lib\net45\ClientDependency.Core.dll - True - ../packages/log4net.2.0.8/lib/net45-full/log4net.dll True diff --git a/src/Umbraco.Web.UI/packages.config b/src/Umbraco.Web.UI/packages.config index 37bc562410..1b519c30ad 100644 --- a/src/Umbraco.Web.UI/packages.config +++ b/src/Umbraco.Web.UI/packages.config @@ -1,7 +1,7 @@  - + diff --git a/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs b/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs index ac61dd790d..2dfc30e37d 100644 --- a/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs +++ b/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using System.Web; using Newtonsoft.Json; -using NPoco; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Sync; @@ -39,7 +38,6 @@ namespace Umbraco.Web internal void Startup() { UmbracoModule.EndRequest += UmbracoModule_EndRequest; - UmbracoModule.RouteAttempt += UmbracoModule_RouteAttempt; if (_databaseFactory.CanConnect == false) { @@ -52,23 +50,6 @@ namespace Umbraco.Web } } - private void UmbracoModule_RouteAttempt(object sender, RoutableAttemptEventArgs e) - { - // as long as umbraco is ready & configured, sync - switch (e.Outcome) - { - case EnsureRoutableOutcome.IsRoutable: - case EnsureRoutableOutcome.NotDocumentRequest: - case EnsureRoutableOutcome.NoContent: - Sync(); - break; - //case EnsureRoutableOutcome.NotReady: - //case EnsureRoutableOutcome.NotConfigured: - //default: - // break; - } - } - private void UmbracoModule_EndRequest(object sender, UmbracoRequestEventArgs e) { // will clear the batch - will remain in HttpContext though - that's ok @@ -108,7 +89,8 @@ namespace Umbraco.Web { UtcStamp = DateTime.UtcNow, Instructions = JsonConvert.SerializeObject(instructions, Formatting.None), - OriginIdentity = LocalIdentity + OriginIdentity = LocalIdentity, + InstructionCount = instructions.Sum(x => x.JsonIdCount) }; using (var scope = ScopeProvider.CreateScope()) diff --git a/src/Umbraco.Web/Cache/CacheRefresherComponent.cs b/src/Umbraco.Web/Cache/CacheRefresherComponent.cs index ef1665e3c2..9d912283a6 100644 --- a/src/Umbraco.Web/Cache/CacheRefresherComponent.cs +++ b/src/Umbraco.Web/Cache/CacheRefresherComponent.cs @@ -32,7 +32,7 @@ namespace Umbraco.Web.Cache [RequiredComponent(typeof(IUmbracoCoreComponent))] // runs before every other IUmbracoCoreComponent! public class CacheRefresherComponent : UmbracoComponentBase, IUmbracoCoreComponent { - private static readonly ConcurrentDictionary FoundHandlers = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary FoundHandlers = new ConcurrentDictionary(); private DistributedCache _distributedCache; private List _unbinders; @@ -195,7 +195,7 @@ namespace Umbraco.Web.Cache { var name = eventDefinition.Sender.GetType().Name + "_" + eventDefinition.EventName; - return FoundHandlers.GetOrAdd(eventDefinition, _ => CandidateHandlers.Value.FirstOrDefault(x => x.Name == name)); + return FoundHandlers.GetOrAdd(name, n => CandidateHandlers.Value.FirstOrDefault(x => x.Name == n)); } private static readonly Lazy CandidateHandlers = new Lazy(() => @@ -480,10 +480,10 @@ namespace Umbraco.Web.Cache _distributedCache.RemoveUserCache(entity.Id); } - private void UserService_SavedUserGroup(IUserService sender, SaveEventArgs e) + private void UserService_SavedUserGroup(IUserService sender, SaveEventArgs e) { foreach (var entity in e.SavedEntities) - _distributedCache.RefreshUserGroupCache(entity.Id); + _distributedCache.RefreshUserGroupCache(entity.UserGroup.Id); } private void UserService_DeletedUserGroup(IUserService sender, DeleteEventArgs e) diff --git a/src/Umbraco.Web/Cache/MemberGroupCacheRefresher.cs b/src/Umbraco.Web/Cache/MemberGroupCacheRefresher.cs index b23cdb5b5a..4540f0b495 100644 --- a/src/Umbraco.Web/Cache/MemberGroupCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/MemberGroupCacheRefresher.cs @@ -1,24 +1,16 @@ using System; using System.Linq; using Newtonsoft.Json; -using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Models; -using Umbraco.Core.Persistence.Repositories; -using Umbraco.Core.Persistence.Repositories.Implement; -using Umbraco.Core.Services; namespace Umbraco.Web.Cache { public sealed class MemberGroupCacheRefresher : JsonCacheRefresherBase { - private readonly IMemberGroupService _memberGroupService; - - public MemberGroupCacheRefresher(CacheHelper cacheHelper, IMemberGroupService memberGroupService) + public MemberGroupCacheRefresher(CacheHelper cacheHelper) : base(cacheHelper) - { - _memberGroupService = memberGroupService; - } + { } #region Define @@ -36,39 +28,28 @@ namespace Umbraco.Web.Cache public override void Refresh(string json) { - var payload = Deserialize(json); - ClearCache(payload); + ClearCache(); base.Refresh(json); } public override void Refresh(int id) { - var group = _memberGroupService.GetById(id); - if (group != null) - ClearCache(new JsonPayload(group.Id, group.Name)); + ClearCache(); base.Refresh(id); } public override void Remove(int id) { - var group = _memberGroupService.GetById(id); - if (group != null) - ClearCache(new JsonPayload(group.Id, group.Name)); + ClearCache(); base.Remove(id); } - private void ClearCache(params JsonPayload[] payloads) + private void ClearCache() { - if (payloads == null) return; - - var memberGroupCache = CacheHelper.IsolatedRuntimeCache.GetCache(); - if (memberGroupCache == false) return; - - foreach (var payload in payloads.WhereNotNull()) - { - memberGroupCache.Result.ClearCacheByKeySearch($"{typeof(IMemberGroup).FullName}.{payload.Name}"); - memberGroupCache.Result.ClearCacheItem(RepositoryCacheKeys.GetKey(payload.Id)); - } + // Since we cache by group name, it could be problematic when renaming to + // previously existing names - see http://issues.umbraco.org/issue/U4-10846. + // To work around this, just clear all the cache items + CacheHelper.IsolatedRuntimeCache.ClearCache(); } #endregion diff --git a/src/Umbraco.Web/Strategies/DatabaseServerRegistrarAndMessengerComponent.cs b/src/Umbraco.Web/Components/DatabaseServerRegistrarAndMessengerComponent.cs similarity index 70% rename from src/Umbraco.Web/Strategies/DatabaseServerRegistrarAndMessengerComponent.cs rename to src/Umbraco.Web/Components/DatabaseServerRegistrarAndMessengerComponent.cs index 14f83c44eb..2048ba9e58 100644 --- a/src/Umbraco.Web/Strategies/DatabaseServerRegistrarAndMessengerComponent.cs +++ b/src/Umbraco.Web/Components/DatabaseServerRegistrarAndMessengerComponent.cs @@ -2,8 +2,8 @@ using System.Collections.Generic; using System.Linq; using System.Threading; -using System.Threading.Tasks; using Examine; +using LightInject; using Umbraco.Core; using Umbraco.Core.Components; using Umbraco.Core.Configuration; @@ -14,13 +14,11 @@ using Umbraco.Core.Services; using Umbraco.Core.Services.Changes; using Umbraco.Core.Sync; using Umbraco.Web.Cache; +using Umbraco.Web.Composing; using Umbraco.Web.Routing; using Umbraco.Web.Scheduling; -using LightInject; -using Umbraco.Core.Exceptions; -using Umbraco.Web.Composing; -namespace Umbraco.Web.Strategies +namespace Umbraco.Web.Components { /// /// Ensures that servers are automatically registered in the database, when using the database server registrar. @@ -38,13 +36,14 @@ namespace Umbraco.Web.Strategies { private object _locker = new object(); private DatabaseServerRegistrar _registrar; + private BatchedDatabaseServerMessenger _messenger; private IRuntimeState _runtime; private ILogger _logger; private IServerRegistrationService _registrationService; - private BackgroundTaskRunner _backgroundTaskRunner; + private BackgroundTaskRunner _touchTaskRunner; + private BackgroundTaskRunner _processTaskRunner; private bool _started; - private TouchServerTask _task; - private IUmbracoDatabaseFactory _databaseFactory; + private IBackgroundTask[] _tasks; public override void Compose(Composition composition) { @@ -102,24 +101,27 @@ namespace Umbraco.Web.Strategies indexer.Value.RebuildIndex(); } - public void Initialize(IRuntimeState runtime, IServerRegistrar serverRegistrar, IServerRegistrationService registrationService, IUmbracoDatabaseFactory databaseFactory, ILogger logger) + public void Initialize(IRuntimeState runtime, IServerRegistrar serverRegistrar, IServerMessenger serverMessenger, IServerRegistrationService registrationService, ILogger logger) { if (UmbracoConfig.For.UmbracoSettings().DistributedCall.Enabled) return; _registrar = serverRegistrar as DatabaseServerRegistrar; if (_registrar == null) throw new Exception("panic: registar."); + _messenger = serverMessenger as BatchedDatabaseServerMessenger; + if (_messenger == null) throw new Exception("panic: messenger"); + _runtime = runtime; - _databaseFactory = databaseFactory; _logger = logger; _registrationService = registrationService; - _backgroundTaskRunner = new BackgroundTaskRunner( - new BackgroundTaskRunnerOptions { AutoStart = true }, - logger); + _touchTaskRunner = new BackgroundTaskRunner("ServerRegistration", + new BackgroundTaskRunnerOptions { AutoStart = true }, logger); + _processTaskRunner = new BackgroundTaskRunner("ServerInstProcess", + new BackgroundTaskRunnerOptions { AutoStart = true }, logger); //We will start the whole process when a successful request is made - UmbracoModule.RouteAttempt += UmbracoModuleRouteAttempt; + UmbracoModule.RouteAttempt += RegisterBackgroundTasksOnce; } /// @@ -133,70 +135,95 @@ namespace Umbraco.Web.Strategies /// - RegisterServer is called on UmbracoModule.RouteAttempt which is triggered in ProcessRequest /// we are safe, UmbracoApplicationUrl has been initialized /// - private void UmbracoModuleRouteAttempt(object sender, RoutableAttemptEventArgs e) + private void RegisterBackgroundTasksOnce(object sender, RoutableAttemptEventArgs e) { switch (e.Outcome) { case EnsureRoutableOutcome.IsRoutable: case EnsureRoutableOutcome.NotDocumentRequest: - RegisterBackgroundTasks(e); + UmbracoModule.RouteAttempt -= RegisterBackgroundTasksOnce; + RegisterBackgroundTasks(); break; } } - private void RegisterBackgroundTasks(UmbracoRequestEventArgs e) + private void RegisterBackgroundTasks() { - // remove handler, we're done - UmbracoModule.RouteAttempt -= UmbracoModuleRouteAttempt; - // only perform this one time ever - LazyInitializer.EnsureInitialized(ref _task, ref _started, ref _locker, () => + LazyInitializer.EnsureInitialized(ref _tasks, ref _started, ref _locker, () => { var serverAddress = _runtime.ApplicationUrl.ToString(); - var svc = _registrationService; - var task = new TouchServerTask(_backgroundTaskRunner, - 15000, //delay before first execution - _registrar.Options.RecurringSeconds*1000, //amount of ms between executions - svc, _registrar, serverAddress, _databaseFactory, _logger); - - // perform the rest async, we don't want to block the startup sequence - // this will just reoccur on a background thread - _backgroundTaskRunner.TryAdd(task); - - return task; + return new[] + { + RegisterInstructionProcess(), + RegisterTouchServer(_registrationService, serverAddress) + }; }); } + private IBackgroundTask RegisterInstructionProcess() + { + var task = new InstructionProcessTask(_processTaskRunner, + 60000, //delay before first execution + _messenger.Options.ThrottleSeconds*1000, //amount of ms between executions + _messenger); + _processTaskRunner.TryAdd(task); + return task; + } + + private IBackgroundTask RegisterTouchServer(IServerRegistrationService registrationService, string serverAddress) + { + var task = new TouchServerTask(_touchTaskRunner, + 15000, //delay before first execution + _registrar.Options.RecurringSeconds*1000, //amount of ms between executions + registrationService, _registrar, serverAddress, _logger); + _touchTaskRunner.TryAdd(task); + return task; + } + + private class InstructionProcessTask : RecurringTaskBase + { + private readonly DatabaseServerMessenger _messenger; + + public InstructionProcessTask(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, + DatabaseServerMessenger messenger) + : base(runner, delayMilliseconds, periodMilliseconds) + { + _messenger = messenger; + } + + public override bool IsAsync => false; + + /// + /// Runs the background task. + /// + /// A value indicating whether to repeat the task. + public override bool PerformRun() + { + // TODO what happens in case of an exception? + _messenger.Sync(); + return true; // repeat + } + } + private class TouchServerTask : RecurringTaskBase { private readonly IServerRegistrationService _svc; private readonly DatabaseServerRegistrar _registrar; private readonly string _serverAddress; - private readonly IUmbracoDatabaseFactory _databaseFactory; private readonly ILogger _logger; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - /// The task runner. - /// The delay. - /// The period. - /// - /// - /// - /// - /// - /// The task will repeat itself periodically. Use this constructor to create a new task. public TouchServerTask(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, - IServerRegistrationService svc, DatabaseServerRegistrar registrar, string serverAddress, IUmbracoDatabaseFactory databaseFactory, ILogger logger) + IServerRegistrationService svc, DatabaseServerRegistrar registrar, string serverAddress, ILogger logger) : base(runner, delayMilliseconds, periodMilliseconds) { - if (svc == null) throw new ArgumentNullException(nameof(svc)); - _svc = svc; + _svc = svc ?? throw new ArgumentNullException(nameof(svc)); _registrar = registrar; _serverAddress = serverAddress; - _databaseFactory = databaseFactory; _logger = logger; } diff --git a/src/Umbraco.Web/Strategies/LegacyServerRegistrarAndMessengerComponent.cs b/src/Umbraco.Web/Components/LegacyServerRegistrarAndMessengerComponent.cs similarity index 90% rename from src/Umbraco.Web/Strategies/LegacyServerRegistrarAndMessengerComponent.cs rename to src/Umbraco.Web/Components/LegacyServerRegistrarAndMessengerComponent.cs index 327be338a5..d494d239a3 100644 --- a/src/Umbraco.Web/Strategies/LegacyServerRegistrarAndMessengerComponent.cs +++ b/src/Umbraco.Web/Components/LegacyServerRegistrarAndMessengerComponent.cs @@ -1,15 +1,19 @@ using System; +using LightInject; using Umbraco.Core; using Umbraco.Core.Components; using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Core.Services; -using LightInject; using Umbraco.Web.Runtime; -namespace Umbraco.Web.Strategies +namespace Umbraco.Web.Components { + // the legacy LB is not enabled by default, because LB is implemented by + // DatabaseServerRegistrarAndMessengerComponent instead + [RuntimeLevel(MinLevel = RuntimeLevel.Run)] + [DisableComponent] // is not enabled by default public sealed class LegacyServerRegistrarAndMessengerComponent : UmbracoComponentBase, IUmbracoCoreComponent { public override void Compose(Composition composition) diff --git a/src/Umbraco.Web/Strategies/NotificationsComponent.cs b/src/Umbraco.Web/Components/NotificationsComponent.cs similarity index 98% rename from src/Umbraco.Web/Strategies/NotificationsComponent.cs rename to src/Umbraco.Web/Components/NotificationsComponent.cs index b56fb75df0..292bb4bf58 100644 --- a/src/Umbraco.Web/Strategies/NotificationsComponent.cs +++ b/src/Umbraco.Web/Components/NotificationsComponent.cs @@ -1,13 +1,13 @@ using System.Collections.Generic; using Umbraco.Core; -using Umbraco.Core.Services; using Umbraco.Core.Components; -using Umbraco.Web._Legacy.Actions; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; +using Umbraco.Core.Services; using Umbraco.Core.Services.Implement; +using Umbraco.Web._Legacy.Actions; -namespace Umbraco.Web.Strategies +namespace Umbraco.Web.Components { [RuntimeLevel(MinLevel = RuntimeLevel.Run)] public sealed class NotificationsComponent : UmbracoComponentBase, IUmbracoCoreComponent diff --git a/src/Umbraco.Web/Strategies/PublicAccessComponent.cs b/src/Umbraco.Web/Components/PublicAccessComponent.cs similarity index 97% rename from src/Umbraco.Web/Strategies/PublicAccessComponent.cs rename to src/Umbraco.Web/Components/PublicAccessComponent.cs index 87bab76ebb..5c18eefc66 100644 --- a/src/Umbraco.Web/Strategies/PublicAccessComponent.cs +++ b/src/Umbraco.Web/Components/PublicAccessComponent.cs @@ -4,7 +4,7 @@ using Umbraco.Core.Services; using Umbraco.Core.Services.Implement; using Umbraco.Web.Composing; -namespace Umbraco.Web.Strategies +namespace Umbraco.Web.Components { /// /// Used to ensure that the public access data file is kept up to date properly diff --git a/src/Umbraco.Web/Composing/CompositionRoots/WebMappingProfilesCompositionRoot.cs b/src/Umbraco.Web/Composing/CompositionRoots/WebMappingProfilesCompositionRoot.cs index 5ac4904712..56b61f1fc9 100644 --- a/src/Umbraco.Web/Composing/CompositionRoots/WebMappingProfilesCompositionRoot.cs +++ b/src/Umbraco.Web/Composing/CompositionRoots/WebMappingProfilesCompositionRoot.cs @@ -19,8 +19,9 @@ namespace Umbraco.Web.Composing.CompositionRoots container.Register(); container.Register(); container.Register(); - container.Register(); container.Register(); + container.Register(); + container.Register(); } } } diff --git a/src/Umbraco.Web/Controllers/UmbRegisterController.cs b/src/Umbraco.Web/Controllers/UmbRegisterController.cs index 0d622b8730..64f54dd465 100644 --- a/src/Umbraco.Web/Controllers/UmbRegisterController.cs +++ b/src/Umbraco.Web/Controllers/UmbRegisterController.cs @@ -17,6 +17,13 @@ namespace Umbraco.Web.Controllers return CurrentUmbracoPage(); } + // U4-10762 Server error with "Register Member" snippet (Cannot save member with empty name) + // If name field is empty, add the email address instead + if (string.IsNullOrEmpty(model.Name) && string.IsNullOrEmpty(model.Email) == false) + { + model.Name = model.Email; + } + MembershipCreateStatus status; var member = Members.RegisterMember(model, out status, model.LoginOnSuccess); diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index 10ec705b74..e15c371d20 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Net; using System.Net.Http; using System.Collections.Generic; +using System.Security.Principal; using System.Threading.Tasks; using System.Web; using System.Web.Http; @@ -214,6 +215,7 @@ namespace Umbraco.Web.Editors public async Task PostLogin(LoginModel loginModel) { var http = EnsureHttpContext(); + var owinContext = TryGetOwinContext().Result; //Sign the user in with username/password, this also gives a chance for developers to //custom verify the credentials and auto-link user accounts with a custom IBackOfficePasswordChecker @@ -228,7 +230,7 @@ namespace Umbraco.Web.Editors var user = Services.UserService.GetByUsername(loginModel.Username); UserManager.RaiseLoginSuccessEvent(user.Id); - return SetPrincipalAndReturnUserDetail(user); + return SetPrincipalAndReturnUserDetail(user, owinContext.Request.User); case SignInStatus.RequiresVerification: var twofactorOptions = UserManager as IUmbracoBackOfficeTwoFactorOptions; @@ -241,7 +243,7 @@ namespace Umbraco.Web.Editors } var twofactorView = twofactorOptions.GetTwoFactorView( - TryGetOwinContext().Result, + owinContext, UmbracoContext, loginModel.Username); @@ -372,13 +374,14 @@ namespace Umbraco.Web.Editors } var result = await SignInManager.TwoFactorSignInAsync(model.Provider, model.Code, isPersistent: true, rememberBrowser: false); + var owinContext = TryGetOwinContext().Result; var user = Services.UserService.GetByUsername(userName); switch (result) { case SignInStatus.Success: UserManager.RaiseLoginSuccessEvent(user.Id); - return SetPrincipalAndReturnUserDetail(user); + return SetPrincipalAndReturnUserDetail(user, owinContext.Request.User); case SignInStatus.LockedOut: UserManager.RaiseAccountLockedEvent(user.Id); return Request.CreateValidationErrorResponse("User is locked out"); @@ -437,13 +440,15 @@ namespace Umbraco.Web.Editors [ValidateAngularAntiForgeryToken] public HttpResponseMessage PostLogout() { - Request.TryGetOwinContext().Result.Authentication.SignOut( + var owinContext = Request.TryGetOwinContext().Result; + + owinContext.Authentication.SignOut( Core.Constants.Security.BackOfficeAuthenticationType, Core.Constants.Security.BackOfficeExternalAuthenticationType); Logger.Info("User {0} from IP address {1} has logged out", () => User.Identity == null ? "UNKNOWN" : User.Identity.Name, - () => TryGetOwinContext().Result.Request.RemoteIpAddress); + () => owinContext.Request.RemoteIpAddress); if (UserManager != null) { @@ -459,10 +464,12 @@ namespace Umbraco.Web.Editors /// This is used when the user is auth'd successfully and we need to return an OK with user details along with setting the current Principal in the request /// /// + /// /// - private HttpResponseMessage SetPrincipalAndReturnUserDetail(IUser user) + private HttpResponseMessage SetPrincipalAndReturnUserDetail(IUser user, IPrincipal principal) { if (user == null) throw new ArgumentNullException("user"); + if (principal == null) throw new ArgumentNullException(nameof(principal)); var userDetail = Mapper.Map(user); //update the userDetail and set their remaining seconds @@ -472,7 +479,7 @@ namespace Umbraco.Web.Editors var response = Request.CreateResponse(HttpStatusCode.OK, userDetail); //ensure the user is set for the current request - Request.SetPrincipalForRequest(user); + Request.SetPrincipalForRequest(principal); return response; } diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index d1c3009792..b31e870ca4 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -375,6 +375,19 @@ namespace Umbraco.Web.Editors if (loginInfo == null) throw new ArgumentNullException("loginInfo"); if (response == null) throw new ArgumentNullException("response"); + + //Here we can check if the provider associated with the request has been configured to allow + // new users (auto-linked external accounts). This would never be used with public providers such as + // Google, unless you for some reason wanted anybody to be able to access the backend if they have a Google account + // .... not likely! + var authType = OwinContext.Authentication.GetExternalAuthenticationTypes().FirstOrDefault(x => x.AuthenticationType == loginInfo.Login.LoginProvider); + if (authType == null) + { + Logger.Warn("Could not find external authentication provider registered: " + loginInfo.Login.LoginProvider); + } + + var autoLinkOptions = authType.GetExternalAuthenticationOptions(); + // Sign in the user with this external login provider if the user already has a login var user = await UserManager.FindAsync(loginInfo.Login); if (user != null) @@ -385,12 +398,25 @@ namespace Umbraco.Web.Editors // ticket format, etc.. to create our back office user including the claims assigned and in this method we'd just ensure // that the ticket is created and stored and that the user is logged in. - //sign in - await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false); + var shouldSignIn = true; + if (autoLinkOptions != null && autoLinkOptions.OnExternalLogin != null) + { + shouldSignIn = autoLinkOptions.OnExternalLogin(user, loginInfo); + if (shouldSignIn == false) + { + Logger.Warn("The AutoLinkOptions of the external authentication provider '" + loginInfo.Login.LoginProvider + "' have refused the login based on the OnExternalLogin method. Affected user id: '" + user.Id + "'"); + } + } + + if (shouldSignIn) + { + //sign in + await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false); + } } else { - if (await AutoLinkAndSignInExternalAccount(loginInfo) == false) + if (await AutoLinkAndSignInExternalAccount(loginInfo, autoLinkOptions) == false) { ViewData[TokenExternalSignInError] = new[] { "The requested provider (" + loginInfo.Login.LoginProvider + ") has not been linked to to an account" }; } @@ -405,97 +431,81 @@ namespace Umbraco.Web.Editors return response(); } - private async Task AutoLinkAndSignInExternalAccount(ExternalLoginInfo loginInfo) + private async Task AutoLinkAndSignInExternalAccount(ExternalLoginInfo loginInfo, ExternalSignInAutoLinkOptions autoLinkOptions) { - //Here we can check if the provider associated with the request has been configured to allow - // new users (auto-linked external accounts). This would never be used with public providers such as - // Google, unless you for some reason wanted anybody to be able to access the backend if they have a Google account - // .... not likely! - - var authType = OwinContext.Authentication.GetExternalAuthenticationTypes().FirstOrDefault(x => x.AuthenticationType == loginInfo.Login.LoginProvider); - if (authType == null) - { - Logger.Warn("Could not find external authentication provider registered: " + loginInfo.Login.LoginProvider); + if (autoLinkOptions == null) return false; - } - var autoLinkOptions = authType.GetExternalAuthenticationOptions(); - if (autoLinkOptions != null) + if (autoLinkOptions.ShouldAutoLinkExternalAccount(UmbracoContext, loginInfo) == false) + return true; + + //we are allowing auto-linking/creating of local accounts + if (loginInfo.Email.IsNullOrWhiteSpace()) { - if (autoLinkOptions.ShouldAutoLinkExternalAccount(UmbracoContext, loginInfo)) + ViewData[TokenExternalSignInError] = new[] { "The requested provider (" + loginInfo.Login.LoginProvider + ") has not provided an email address, the account cannot be linked." }; + } + else + { + //Now we need to perform the auto-link, so first we need to lookup/create a user with the email address + var foundByEmail = Services.UserService.GetByEmail(loginInfo.Email); + if (foundByEmail != null) { - //we are allowing auto-linking/creating of local accounts - if (loginInfo.Email.IsNullOrWhiteSpace()) + ViewData[TokenExternalSignInError] = new[] { "A user with this email address already exists locally. You will need to login locally to Umbraco and link this external provider: " + loginInfo.Login.LoginProvider }; + } + else + { + if (loginInfo.Email.IsNullOrWhiteSpace()) throw new InvalidOperationException("The Email value cannot be null"); + if (loginInfo.ExternalIdentity.Name.IsNullOrWhiteSpace()) throw new InvalidOperationException("The Name value cannot be null"); + + var groups = Services.UserService.GetUserGroupsByAlias(autoLinkOptions.GetDefaultUserGroups(UmbracoContext, loginInfo)); + + var autoLinkUser = BackOfficeIdentityUser.CreateNew( + loginInfo.Email, + loginInfo.Email, + autoLinkOptions.GetDefaultCulture(UmbracoContext, loginInfo)); + autoLinkUser.Name = loginInfo.ExternalIdentity.Name; + foreach (var userGroup in groups) { - ViewData[TokenExternalSignInError] = new[] { "The requested provider (" + loginInfo.Login.LoginProvider + ") has not provided an email address, the account cannot be linked." }; + autoLinkUser.AddRole(userGroup.Alias); + } + + //call the callback if one is assigned + if (autoLinkOptions.OnAutoLinking != null) + { + autoLinkOptions.OnAutoLinking(autoLinkUser, loginInfo); + } + + var userCreationResult = await UserManager.CreateAsync(autoLinkUser); + + if (userCreationResult.Succeeded == false) + { + ViewData[TokenExternalSignInError] = userCreationResult.Errors; } else { - - //Now we need to perform the auto-link, so first we need to lookup/create a user with the email address - var foundByEmail = Services.UserService.GetByEmail(loginInfo.Email); - if (foundByEmail != null) + var linkResult = await UserManager.AddLoginAsync(autoLinkUser.Id, loginInfo.Login); + if (linkResult.Succeeded == false) { - ViewData[TokenExternalSignInError] = new[] { "A user with this email address already exists locally. You will need to login locally to Umbraco and link this external provider: " + loginInfo.Login.LoginProvider }; + ViewData[TokenExternalSignInError] = linkResult.Errors; + + //If this fails, we should really delete the user since it will be in an inconsistent state! + var deleteResult = await UserManager.DeleteAsync(autoLinkUser); + if (deleteResult.Succeeded == false) + { + //DOH! ... this isn't good, combine all errors to be shown + ViewData[TokenExternalSignInError] = linkResult.Errors.Concat(deleteResult.Errors); + } } else { - if (loginInfo.Email.IsNullOrWhiteSpace()) throw new InvalidOperationException("The Email value cannot be null"); - if (loginInfo.ExternalIdentity.Name.IsNullOrWhiteSpace()) throw new InvalidOperationException("The Name value cannot be null"); - - var groups = Services.UserService.GetUserGroupsByAlias(autoLinkOptions.GetDefaultUserGroups(UmbracoContext, loginInfo)); - - var autoLinkUser = BackOfficeIdentityUser.CreateNew( - loginInfo.Email, - loginInfo.Email, - autoLinkOptions.GetDefaultCulture(UmbracoContext, loginInfo)); - autoLinkUser.Name = loginInfo.ExternalIdentity.Name; - foreach (var userGroup in groups) - { - autoLinkUser.AddRole(userGroup.Alias); - } - - //call the callback if one is assigned - if (autoLinkOptions.OnAutoLinking != null) - { - autoLinkOptions.OnAutoLinking(autoLinkUser, loginInfo); - } - - var userCreationResult = await UserManager.CreateAsync(autoLinkUser); - - if (userCreationResult.Succeeded == false) - { - ViewData[TokenExternalSignInError] = userCreationResult.Errors; - } - else - { - var linkResult = await UserManager.AddLoginAsync(autoLinkUser.Id, loginInfo.Login); - if (linkResult.Succeeded == false) - { - ViewData[TokenExternalSignInError] = linkResult.Errors; - - //If this fails, we should really delete the user since it will be in an inconsistent state! - var deleteResult = await UserManager.DeleteAsync(autoLinkUser); - if (deleteResult.Succeeded == false) - { - //DOH! ... this isn't good, combine all errors to be shown - ViewData[TokenExternalSignInError] = linkResult.Errors.Concat(deleteResult.Errors); - } - } - else - { - //sign in - await SignInManager.SignInAsync(autoLinkUser, isPersistent: false, rememberBrowser: false); - } - } + //sign in + await SignInManager.SignInAsync(autoLinkUser, isPersistent: false, rememberBrowser: false); } - } } - return true; - } - return false; + } + return true; } /// diff --git a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs index c4a893235c..6d080ac048 100644 --- a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs @@ -118,6 +118,10 @@ namespace Umbraco.Web.Editors "redirectUrlManagementApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( controller => controller.GetEnableState()) }, + { + "tourApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( + controller => controller.GetTours()) + }, { "embedApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( controller => controller.GetEmbed("", 0, 0)) @@ -269,6 +273,10 @@ namespace Umbraco.Web.Editors { "nuCacheStatusBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( controller => controller.GetStatus()) + }, + { + "helpApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( + controller => controller.GetContextHelpForPage("","","")) } } }, @@ -378,9 +386,6 @@ namespace Umbraco.Web.Editors /// private Dictionary GetApplicationState() { - if (_runtimeState.Level != RuntimeLevel.Run) - return null; - var app = new Dictionary { {"assemblyVersion", UmbracoVersion.AssemblyVersion} @@ -388,7 +393,9 @@ namespace Umbraco.Web.Editors var version = _runtimeState.SemanticVersion.ToSemanticString(); - app.Add("cacheBuster", $"{version}.{ClientDependencySettings.Instance.Version}".GenerateHash()); + //the value is the hash of the version, cdf version and the configured state + app.Add("cacheBuster", $"{version}.{_runtimeState.Level}.{ClientDependencySettings.Instance.Version}".GenerateHash()); + app.Add("version", version); //useful for dealing with virtual paths on the client side when hosted in virtual directories especially diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index f8bf6b083c..6cae27bb3d 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -71,7 +71,7 @@ namespace Umbraco.Web.Editors public IEnumerable GetByIds([FromUri]int[] ids) { var foundContent = Services.ContentService.GetByIds(ids); - return foundContent.Select(Mapper.Map); + return foundContent.Select(x => ContextMapper.Map(x, UmbracoContext)); } /// @@ -223,7 +223,7 @@ namespace Umbraco.Web.Editors HandleContentNotFound(id); } - var content = Mapper.Map(foundContent); + var content = ContextMapper.Map(foundContent, UmbracoContext); SetupBlueprint(content, foundContent); @@ -261,7 +261,7 @@ namespace Umbraco.Web.Editors HandleContentNotFound(id); } - var content = Mapper.Map(foundContent); + var content = ContextMapper.Map(foundContent, UmbracoContext); return content; } @@ -274,7 +274,7 @@ namespace Umbraco.Web.Editors HandleContentNotFound(id); } - var content = Mapper.Map(foundContent); + var content = ContextMapper.Map(foundContent, UmbracoContext); return content; } @@ -297,7 +297,7 @@ namespace Umbraco.Web.Editors } var emptyContent = Services.ContentService.Create("", parentId, contentType.Alias, Security.GetUserId().ResultOr(0)); - var mapped = Mapper.Map(emptyContent); + var mapped = ContextMapper.Map(emptyContent, UmbracoContext); //remove this tab if it exists: umbContainerView var containerTab = mapped.Tabs.FirstOrDefault(x => x.Alias == Constants.Conventions.PropertyGroups.ListViewGroupName); @@ -433,9 +433,17 @@ namespace Umbraco.Web.Editors [HttpPost] public Dictionary GetPermissions(int[] nodeIds) { - return Services.UserService - .GetPermissions(Security.CurrentUser, nodeIds) - .ToDictionary(x => x.EntityId, x => x.AssignedPermissions); + var permissions = Services.UserService + .GetPermissions(Security.CurrentUser, nodeIds); + + var permissionsDictionary = new Dictionary(); + foreach (var nodeId in nodeIds) + { + var aggregatePerms = permissions.GetAllPermissions(nodeId).ToArray(); + permissionsDictionary.Add(nodeId, aggregatePerms); + } + + return permissionsDictionary; } /// @@ -525,6 +533,7 @@ namespace Umbraco.Web.Editors /// [FileUploadCleanupFilter] [ContentPostValidate] + [OutgoingEditorModelEvent] public ContentItemDisplay PostSave([ModelBinder(typeof(ContentItemBinder))] ContentItemSave contentItem) { return PostSaveInternal(contentItem, content => Services.ContentService.Save(contentItem.PersistedContent, Security.CurrentUser.Id)); @@ -552,7 +561,7 @@ namespace Umbraco.Web.Editors { //ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue! // add the modelstate to the outgoing object and throw a validation message - var forDisplay = Mapper.Map(contentItem.PersistedContent); + var forDisplay = ContextMapper.Map(contentItem.PersistedContent, UmbracoContext); forDisplay.Errors = ModelState.ToErrorDictionary(); throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); @@ -595,7 +604,7 @@ namespace Umbraco.Web.Editors } //return the updated model - var display = Mapper.Map(contentItem.PersistedContent); + var display = ContextMapper.Map(contentItem.PersistedContent, UmbracoContext); //lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403 HandleInvalidModelState(display); @@ -635,8 +644,6 @@ namespace Umbraco.Web.Editors break; } - UpdatePreviewContext(contentItem.PersistedContent.Id); - //If the item is new and the operation was cancelled, we need to return a different // status code so the UI can handle it since it won't be able to redirect since there // is no Id to redirect to! @@ -786,11 +793,8 @@ namespace Umbraco.Web.Editors { var contentService = Services.ContentService; - // content service GetByIds does order the content items based on the order of Ids passed in - var content = contentService.GetByIds(sorted.IdSortOrder); - // Save content with new sort order and update content xml in db accordingly - if (contentService.Sort(content) == false) + if (contentService.Sort(sorted.IdSortOrder) == false) { Logger.Warn("Content sorting failed, this was probably caused by an event being cancelled"); return Request.CreateValidationErrorResponse("Content sorting failed, this was probably caused by an event being cancelled"); @@ -844,6 +848,7 @@ namespace Umbraco.Web.Editors /// /// [EnsureUserPermissionForContent("id", 'U')] + [OutgoingEditorModelEvent] public ContentItemDisplay PostUnPublish(int id) { var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); @@ -853,7 +858,7 @@ namespace Umbraco.Web.Editors var unpublishResult = Services.ContentService.Unpublish(foundContent, Security.CurrentUser.Id); - var content = Mapper.Map(foundContent); + var content = ContextMapper.Map(foundContent, UmbracoContext); if (unpublishResult.Success == false) { @@ -864,16 +869,6 @@ namespace Umbraco.Web.Editors { content.AddSuccessNotification(Services.TextService.Localize("content/unPublish"), Services.TextService.Localize("speechBubbles/contentUnpublished")); return content; - } - } - - /// - /// Checks if the user is currently in preview mode and if so will update the preview content for this item - /// - /// - private void UpdatePreviewContext(int contentId) - { - _publishedSnapshotService.RefreshPreview(Request.GetPreviewCookieValue(), contentId); } /// diff --git a/src/Umbraco.Web/Editors/CurrentUserController.cs b/src/Umbraco.Web/Editors/CurrentUserController.cs index 0642699b13..7a37a74386 100644 --- a/src/Umbraco.Web/Editors/CurrentUserController.cs +++ b/src/Umbraco.Web/Editors/CurrentUserController.cs @@ -1,4 +1,6 @@ -using System.Net.Http; +using System; +using System.Collections; +using System.Net.Http; using System.Threading.Tasks; using System.Web.Http; using AutoMapper; @@ -8,6 +10,9 @@ using Umbraco.Web.Models; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Mvc; using Umbraco.Web.WebApi; +using System.Linq; +using Newtonsoft.Json; +using Umbraco.Core; using Umbraco.Core.Security; using Umbraco.Web.WebApi.Filters; @@ -20,6 +25,49 @@ namespace Umbraco.Web.Editors [PluginController("UmbracoApi")] public class CurrentUserController : UmbracoAuthorizedJsonController { + /// + /// Saves a tour status for the current user + /// + /// + /// + public IEnumerable PostSetUserTour(UserTourStatus status) + { + if (status == null) throw new ArgumentNullException("status"); + + List userTours; + if (Security.CurrentUser.TourData.IsNullOrWhiteSpace()) + { + userTours = new List {status}; + Security.CurrentUser.TourData = JsonConvert.SerializeObject(userTours); + Services.UserService.Save(Security.CurrentUser); + return userTours; + } + + userTours = JsonConvert.DeserializeObject>(Security.CurrentUser.TourData).ToList(); + var found = userTours.FirstOrDefault(x => x.Alias == status.Alias); + if (found != null) + { + //remove it and we'll replace it next + userTours.Remove(found); + } + userTours.Add(status); + Security.CurrentUser.TourData = JsonConvert.SerializeObject(userTours); + Services.UserService.Save(Security.CurrentUser); + return userTours; + } + + /// + /// Returns the user's tours + /// + /// + public IEnumerable GetUserTours() + { + if (Security.CurrentUser.TourData.IsNullOrWhiteSpace()) + return Enumerable.Empty(); + + var userTours = JsonConvert.DeserializeObject>(Security.CurrentUser.TourData); + return userTours; + } /// /// When a user is invited and they click on the invitation link, they will be partially logged in @@ -86,15 +134,12 @@ namespace Umbraco.Web.Editors { var userMgr = this.TryGetOwinContext().Result.GetBackOfficeUserManager(); - //raise the appropriate event + //raise the reset event + //TODO: I don't think this is required anymore since from 7.7 we no longer display the reset password checkbox since that didn't make sense. if (data.Reset.HasValue && data.Reset.Value) { userMgr.RaisePasswordResetEvent(Security.CurrentUser.Id); } - else - { - userMgr.RaisePasswordChangedEvent(Security.CurrentUser.Id); - } //even if we weren't resetting this, it is the correct value (null), otherwise if we were resetting then it will contain the new pword var result = new ModelWithNotifications(passwordChangeResult.Result.ResetPassword); diff --git a/src/Umbraco.Web/Editors/DashboardHelper.cs b/src/Umbraco.Web/Editors/DashboardHelper.cs index 28df8cf526..b40f289d04 100644 --- a/src/Umbraco.Web/Editors/DashboardHelper.cs +++ b/src/Umbraco.Web/Editors/DashboardHelper.cs @@ -69,6 +69,7 @@ namespace Umbraco.Web.Editors var dashboardControl = new DashboardControl(); var controlPath = control.ControlPath.Trim(); + dashboardControl.Caption = control.PanelCaption; dashboardControl.Path = IOHelper.FindFile(controlPath); if (controlPath.ToLowerInvariant().EndsWith(".ascx".ToLowerInvariant())) dashboardControl.ServerSide = true; diff --git a/src/Umbraco.Web/Editors/DataTypeController.cs b/src/Umbraco.Web/Editors/DataTypeController.cs index 1007107b57..148e31d506 100644 --- a/src/Umbraco.Web/Editors/DataTypeController.cs +++ b/src/Umbraco.Web/Editors/DataTypeController.cs @@ -17,6 +17,7 @@ using Constants = Umbraco.Core.Constants; using System.Net.Http; using System.Text; using Umbraco.Web.Composing; +using Umbraco.Core.Configuration; namespace Umbraco.Web.Editors { @@ -343,8 +344,10 @@ namespace Umbraco.Web.Editors public IDictionary> GetGroupedPropertyEditors() { var datatypes = new List(); + var showDeprecatedPropertyEditors = UmbracoConfig.For.UmbracoSettings().Content.ShowDeprecatedPropertyEditors; var propertyEditors = Current.PropertyEditors; + #error .Where(x=>x.IsDeprecated == false || showDeprecatedPropertyEditors); ??? foreach (var propertyEditor in propertyEditors) { var hasPrevalues = propertyEditor.GetConfigurationEditor().Fields.Any(); diff --git a/src/Umbraco.Web/Editors/EditorModelEventArgs.cs b/src/Umbraco.Web/Editors/EditorModelEventArgs.cs new file mode 100644 index 0000000000..153a2d8786 --- /dev/null +++ b/src/Umbraco.Web/Editors/EditorModelEventArgs.cs @@ -0,0 +1,33 @@ +using System; + +namespace Umbraco.Web.Editors +{ + public sealed class EditorModelEventArgs : EditorModelEventArgs + { + public EditorModelEventArgs(EditorModelEventArgs baseArgs) + : base(baseArgs.Model, baseArgs.UmbracoContext) + { + Model = (T)baseArgs.Model; + } + + public EditorModelEventArgs(T model, UmbracoContext umbracoContext) + : base(model, umbracoContext) + { + Model = model; + } + + public new T Model { get; private set; } + } + + public class EditorModelEventArgs : EventArgs + { + public EditorModelEventArgs(object model, UmbracoContext umbracoContext) + { + Model = model; + UmbracoContext = umbracoContext; + } + + public object Model { get; private set; } + public UmbracoContext UmbracoContext { get; private set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/HelpController.cs b/src/Umbraco.Web/Editors/HelpController.cs new file mode 100644 index 0000000000..7cb393aed6 --- /dev/null +++ b/src/Umbraco.Web/Editors/HelpController.cs @@ -0,0 +1,43 @@ +using Newtonsoft.Json; +using System.Collections.Generic; +using System.Net.Http; +using System.Runtime.Serialization; +using System.Threading.Tasks; + +namespace Umbraco.Web.Editors +{ + public class HelpController : UmbracoAuthorizedJsonController + { + public async Task> GetContextHelpForPage(string section, string tree, string baseUrl = "https://our.umbraco.org") + { + var url = string.Format(baseUrl + "/Umbraco/Documentation/Lessons/GetContextHelpDocs?sectionAlias={0}&treeAlias={1}", section, tree); + using (var web = new HttpClient()) + { + //fetch dashboard json and parse to JObject + var json = await web.GetStringAsync(url); + var result = JsonConvert.DeserializeObject>(json); + if (result != null) + return result; + + return new List(); + } + } + } + + [DataContract(Name = "HelpPage")] + public class HelpPage + { + [DataMember(Name = "name")] + public string Name { get; set; } + + [DataMember(Name = "description")] + public string Description { get; set; } + + [DataMember(Name = "url")] + public string Url { get; set; } + + [DataMember(Name = "type")] + public string Type { get; set; } + + } +} diff --git a/src/Umbraco.Web/Editors/LogController.cs b/src/Umbraco.Web/Editors/LogController.cs index e8898ff84d..aa95af201f 100644 --- a/src/Umbraco.Web/Editors/LogController.cs +++ b/src/Umbraco.Web/Editors/LogController.cs @@ -1,8 +1,13 @@ using System; using System.Collections.Generic; +using System.Net; +using System.Net.Http; using AutoMapper; using Umbraco.Web.Models.ContentEditing; using Umbraco.Core.Models; +using Umbraco.Core.Models; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; +using Umbraco.Core.Persistence.Querying; using Umbraco.Web.Mvc; namespace Umbraco.Web.Editors @@ -13,21 +18,60 @@ namespace Umbraco.Web.Editors [PluginController("UmbracoApi")] public class LogController : UmbracoAuthorizedJsonController { + public PagedResult GetPagedEntityLog(int id, + int pageNumber = 1, + int pageSize = 0, + Direction orderDirection = Direction.Descending, + DateTime? sinceDate = null) + { + long totalRecords; + var dateQuery = sinceDate.HasValue ? Query.Builder.Where(x => x.CreateDate >= sinceDate) : null; + var result = Services.AuditService.GetPagedItemsByEntity(id, pageNumber - 1, pageSize, out totalRecords, orderDirection, customFilter: dateQuery); + var mapped = Mapper.Map>(result); + + var page = new PagedResult(totalRecords, pageNumber, pageSize) + { + Items = MapAvatarsAndNames(mapped) + }; + + return page; + } + + public PagedResult GetPagedCurrentUserLog( + int pageNumber = 1, + int pageSize = 0, + Direction orderDirection = Direction.Descending, + DateTime? sinceDate = null) + { + long totalRecords; + var dateQuery = sinceDate.HasValue ? Query.Builder.Where(x => x.CreateDate >= sinceDate) : null; + var result = Services.AuditService.GetPagedItemsByUser(Security.GetUserId(), pageNumber - 1, pageSize, out totalRecords, orderDirection, customFilter:dateQuery); + var mapped = Mapper.Map>(result); + return new PagedResult(totalRecords, pageNumber + 1, pageSize) + { + Items = MapAvatarsAndNames(mapped) + }; + } + + [Obsolete("Use GetPagedLog instead")] public IEnumerable GetEntityLog(int id) { - return Mapper.Map>( - Services.AuditService.GetLogs(id)); + long totalRecords; + var result = Services.AuditService.GetPagedItemsByEntity(id, 1, int.MaxValue, out totalRecords); + return Mapper.Map>(result); } + //TODO: Move to CurrentUserController? + [Obsolete("Use GetPagedCurrentUserLog instead")] public IEnumerable GetCurrentUserLog(AuditType logType, DateTime? sinceDate) { - if (sinceDate == null) - sinceDate = DateTime.Now.Subtract(new TimeSpan(7, 0, 0, 0, 0)); - - return Mapper.Map>( - Services.AuditService.GetUserLogs(Security.CurrentUser.Id, logType, sinceDate.Value)); + long totalRecords; + var dateQuery = sinceDate.HasValue ? Query.Builder.Where(x => x.CreateDate >= sinceDate) : null; + var result = Services.AuditService.GetPagedItemsByUser(Security.GetUserId(), 1, int.MaxValue, out totalRecords, auditTypeFilter: new[] {logType},customFilter: dateQuery); + return Mapper.Map>(result); } + [Obsolete("Use GetPagedLog instead")] public IEnumerable GetLog(AuditType logType, DateTime? sinceDate) { if (sinceDate == null) @@ -37,5 +81,18 @@ namespace Umbraco.Web.Editors Services.AuditService.GetLogs(logType, sinceDate.Value)); } + private IEnumerable MapAvatarsAndNames(IEnumerable items) + { + var userIds = items.Select(x => x.UserId).ToArray(); + var users = Services.UserService.GetUsersById(userIds) + .ToDictionary(x => x.Id, x => x.GetUserAvatarUrls(ApplicationContext.ApplicationCache.RuntimeCache)); + var userNames = Services.UserService.GetUsersById(userIds).ToDictionary(x => x.Id, x => x.Name); + foreach (var item in items) + { + item.UserAvatars = users[item.UserId]; + item.UserName = userNames[item.UserId]; + } + return items; + } } } diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index 5287b06a00..d306ad33af 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.ComponentModel; -using System.Globalization; using System.IO; using System.Net; using System.Net.Http; @@ -23,9 +22,7 @@ using Umbraco.Web.Models.Mapping; using Umbraco.Web.Mvc; using Umbraco.Web.WebApi; using System.Linq; -using System.Text.RegularExpressions; using System.Web.Http.Controllers; -using Examine; using Umbraco.Web.WebApi.Binders; using Umbraco.Web.WebApi.Filters; using Constants = Umbraco.Core.Constants; @@ -78,7 +75,7 @@ namespace Umbraco.Web.Editors } var emptyContent = Services.MediaService.CreateMedia("", parentId, contentType.Alias, Security.GetUserId().ResultOr(0)); - var mapped = Mapper.Map(emptyContent); + var mapped = ContextMapper.Map(emptyContent, UmbracoContext); //remove this tab if it exists: umbContainerView var containerTab = mapped.Tabs.FirstOrDefault(x => x.Alias == Constants.Conventions.PropertyGroups.ListViewGroupName); @@ -126,7 +123,7 @@ namespace Umbraco.Web.Editors //HandleContentNotFound will throw an exception return null; } - return Mapper.Map(foundContent); + return ContextMapper.Map(foundContent, UmbracoContext); } /// @@ -146,7 +143,7 @@ namespace Umbraco.Web.Editors //HandleContentNotFound will throw an exception return null; } - return Mapper.Map(foundContent); + return ContextMapper.Map(foundContent, UmbracoContext); } /// @@ -175,7 +172,7 @@ namespace Umbraco.Web.Editors public IEnumerable GetByIds([FromUri]int[] ids) { var foundMedia = Services.MediaService.GetByIds(ids); - return foundMedia.Select(Mapper.Map); + return foundMedia.Select(media => ContextMapper.Map(media, UmbracoContext)); } /// @@ -461,6 +458,7 @@ namespace Umbraco.Web.Editors /// [FileUploadCleanupFilter] [MediaPostValidate] + [OutgoingEditorModelEvent] public MediaItemDisplay PostSave( [ModelBinder(typeof(MediaItemBinder))] MediaItemSave contentItem) @@ -487,7 +485,7 @@ namespace Umbraco.Web.Editors { //ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue! // add the modelstate to the outgoing object and throw validation response - var forDisplay = Mapper.Map(contentItem.PersistedContent); + var forDisplay = ContextMapper.Map(contentItem.PersistedContent, UmbracoContext); forDisplay.Errors = ModelState.ToErrorDictionary(); throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); } @@ -497,7 +495,7 @@ namespace Umbraco.Web.Editors var saveStatus = Services.MediaService.WithResult().Save(contentItem.PersistedContent, (int)Security.CurrentUser.Id); //return the updated model - var display = Mapper.Map(contentItem.PersistedContent); + var display = AutoMapperExtensions.MapWithUmbracoContext(contentItem.PersistedContent, UmbracoContext); //lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403 HandleInvalidModelState(display); @@ -596,15 +594,17 @@ namespace Umbraco.Web.Editors throw; } } - - [EnsureUserPermissionForMedia("folder.ParentId")] - public MediaItemDisplay PostAddFolder(EntityBasic folder) + + public MediaItemDisplay PostAddFolder(PostedFolder folder) { + var intParentId = GetParentIdAsInt(folder.ParentId, validatePermissions:true); + var mediaService = Services.MediaService; - var f = mediaService.CreateMedia(folder.Name, folder.ParentId, Constants.Conventions.MediaTypes.Folder); + + var f = mediaService.CreateMedia(folder.Name, intParentId, Constants.Conventions.MediaTypes.Folder); mediaService.Save(f, Security.CurrentUser.Id); - return Mapper.Map(f); + return ContextMapper.Map(f, UmbracoContext); } /// @@ -636,63 +636,12 @@ namespace Umbraco.Web.Editors } //get the string json from the request - int parentId; bool entityFound; GuidUdi parentUdi; string currentFolderId = result.FormData["currentFolder"]; - // test for udi - if (GuidUdi.TryParse(currentFolderId, out parentUdi)) - { - currentFolderId = parentUdi.Guid.ToString(); - } - - if (int.TryParse(currentFolderId, out parentId) == false) - { - // if a guid then try to look up the entity - Guid idGuid; - if (Guid.TryParse(currentFolderId, out idGuid)) - { - var entity = Services.EntityService.Get(idGuid); - if (entity != null) - { - entityFound = true; - parentId = entity.Id; - } - else - { - throw new EntityNotFoundException(currentFolderId, "The passed id doesn't exist"); - } - } - else - { - return Request.CreateValidationErrorResponse("The request was not formatted correctly, the currentFolder is not an integer or Guid"); - } - - if (entityFound == false) - { - return Request.CreateValidationErrorResponse("The request was not formatted correctly, the currentFolder is not an integer or Guid"); - } - } - - - //ensure the user has access to this folder by parent id! - if (CheckPermissions( - new Dictionary(), - Security.CurrentUser, - Services.MediaService, - Services.EntityService, - parentId) == false) - { - return Request.CreateResponse( - HttpStatusCode.Forbidden, - new SimpleNotificationModel(new Notification( - Services.TextService.Localize("speechBubbles/operationFailedHeader"), - Services.TextService.Localize("speechBubbles/invalidUserPermissionsText"), - SpeechBubbleIcon.Warning))); - } - + int parentId = GetParentIdAsInt(currentFolderId, validatePermissions: true); + var tempFiles = new PostedFiles(); - var mediaService = Services.MediaService; - - + var mediaService = ApplicationContext.Services.MediaService; + //in case we pass a path with a folder in it, we will create it and upload media to it. if (result.FormData.ContainsKey("path")) { @@ -827,6 +776,69 @@ namespace Umbraco.Web.Editors return Request.CreateResponse(HttpStatusCode.OK, tempFiles); } + /// + /// Given a parent id which could be a GUID, UDI or an INT, this will resolve the INT + /// + /// + /// + /// If true, this will check if the current user has access to the resolved integer parent id + /// and if that check fails an unauthorized exception will occur + /// + /// + private int GetParentIdAsInt(string parentId, bool validatePermissions) + { + int intParentId; + GuidUdi parentUdi; + + // test for udi + if (GuidUdi.TryParse(parentId, out parentUdi)) + { + parentId = parentUdi.Guid.ToString(); + } + + //if it's not an INT then we'll check for GUID + if (int.TryParse(parentId, out intParentId) == false) + { + // if a guid then try to look up the entity + Guid idGuid; + if (Guid.TryParse(parentId, out idGuid)) + { + var entity = Services.EntityService.Get(idGuid); + if (entity != null) + { + intParentId = entity.Id; + } + else + { + throw new EntityNotFoundException(parentId, "The passed id doesn't exist"); + } + } + else + { + throw new HttpResponseException( + Request.CreateValidationErrorResponse("The request was not formatted correctly, the parentId is not an integer, Guid or UDI")); + } + } + + //ensure the user has access to this folder by parent id! + if (validatePermissions && CheckPermissions( + new Dictionary(), + Security.CurrentUser, + Services.MediaService, + Services.EntityService, + intParentId) == false) + { + throw new HttpResponseException(Request.CreateResponse( + HttpStatusCode.Forbidden, + new SimpleNotificationModel(new Notification( + Services.TextService.Localize("speechBubbles/operationFailedHeader"), + Services.TextService.Localize("speechBubbles/invalidUserPermissionsText"), + SpeechBubbleIcon.Warning)))); + } + + return intParentId; + } + /// /// Ensures the item can be moved/copied to the new location /// diff --git a/src/Umbraco.Web/Editors/MemberController.cs b/src/Umbraco.Web/Editors/MemberController.cs index fa76652b16..8f4b285160 100644 --- a/src/Umbraco.Web/Editors/MemberController.cs +++ b/src/Umbraco.Web/Editors/MemberController.cs @@ -3,6 +3,8 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using System.Net; using System.Net.Http; +using System.Threading.Tasks; +using System.Web; using System.Web.Http; using System.Web.Http.ModelBinding; using System.Web.Security; @@ -62,16 +64,18 @@ namespace Umbraco.Web.Editors if (MembershipScenario == MembershipScenario.NativeUmbraco) { - long totalRecords; var members = Services.MemberService - .GetAll((pageNumber - 1), pageSize, out totalRecords, orderBy, orderDirection, orderBySystemField, memberTypeAlias, filter).ToArray(); + .GetAll((pageNumber - 1), pageSize, out var totalRecords, orderBy, orderDirection, orderBySystemField, memberTypeAlias, filter).ToArray(); if (totalRecords == 0) { return new PagedResult(0, 0, 0); } - var pagedResult = new PagedResult(totalRecords, pageNumber, pageSize); - pagedResult.Items = members - .Select(Mapper.Map); + + var pagedResult = new PagedResult(totalRecords, pageNumber, pageSize) + { + Items = members + .Select(x => ContextMapper.Map(x, UmbracoContext)) + }; return pagedResult; } else @@ -99,10 +103,13 @@ namespace Umbraco.Web.Editors { return new PagedResult(0, 0, 0); } - var pagedResult = new PagedResult(totalRecords, pageNumber, pageSize); - pagedResult.Items = members - .Cast() - .Select(Mapper.Map); + + var pagedResult = new PagedResult(totalRecords, pageNumber, pageSize) + { + Items = members + .Cast() + .Select(Mapper.Map) + }; return pagedResult; } @@ -150,7 +157,7 @@ namespace Umbraco.Web.Editors { HandleContentNotFound(key); } - return Mapper.Map(foundMember); + return ContextMapper.Map(foundMember, UmbracoContext); case MembershipScenario.CustomProviderWithUmbracoLink: //TODO: Support editing custom properties for members with a custom membership provider here. @@ -210,7 +217,7 @@ namespace Umbraco.Web.Editors emptyContent = new Member(contentType); emptyContent.AdditionalData["NewPassword"] = Membership.GeneratePassword(provider.MinRequiredPasswordLength, provider.MinRequiredNonAlphanumericCharacters); - return Mapper.Map(emptyContent); + return ContextMapper.Map(emptyContent, UmbracoContext); case MembershipScenario.CustomProviderWithUmbracoLink: //TODO: Support editing custom properties for members with a custom membership provider here. @@ -219,7 +226,7 @@ namespace Umbraco.Web.Editors //we need to return a scaffold of a 'simple' member - basically just what a membership provider can edit emptyContent = MemberService.CreateGenericMembershipProviderMember("", "", "", ""); emptyContent.AdditionalData["NewPassword"] = Membership.GeneratePassword(Membership.MinRequiredPasswordLength, Membership.MinRequiredNonAlphanumericCharacters); - return Mapper.Map(emptyContent); + return ContextMapper.Map(emptyContent, UmbracoContext); } } @@ -228,6 +235,7 @@ namespace Umbraco.Web.Editors /// /// [FileUploadCleanupFilter] + [OutgoingEditorModelEvent] public MemberDisplay PostSave( [ModelBinder(typeof(MemberBinder))] MemberSave contentItem) @@ -258,7 +266,7 @@ namespace Umbraco.Web.Editors //Unlike content/media - if there are errors for a member, we do NOT proceed to save them, we cannot so return the errors if (ModelState.IsValid == false) { - var forDisplay = Mapper.Map(contentItem.PersistedContent); + var forDisplay = ContextMapper.Map(contentItem.PersistedContent, UmbracoContext); forDisplay.Errors = ModelState.ToErrorDictionary(); throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); } @@ -300,7 +308,7 @@ namespace Umbraco.Web.Editors //If we've had problems creating/updating the user with the provider then return the error if (ModelState.IsValid == false) { - var forDisplay = Mapper.Map(contentItem.PersistedContent); + var forDisplay = ContextMapper.Map(contentItem.PersistedContent, UmbracoContext); forDisplay.Errors = ModelState.ToErrorDictionary(); throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); } @@ -338,7 +346,7 @@ namespace Umbraco.Web.Editors contentItem.PersistedContent.AdditionalData["GeneratedPassword"] = generatedPassword; //return the updated model - var display = Mapper.Map(contentItem.PersistedContent); + var display = ContextMapper.Map(contentItem.PersistedContent, UmbracoContext); //lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403 HandleInvalidModelState(display); @@ -394,6 +402,33 @@ namespace Umbraco.Web.Editors var shouldReFetchMember = false; var providedUserName = contentItem.PersistedContent.Username; + //if the user doesn't have access to sensitive values, then we need to check if any of the built in member property types + //have been marked as sensitive. If that is the case we cannot change these persisted values no matter what value has been posted. + //There's only 3 special ones we need to deal with that are part of the MemberSave instance + if (Security.CurrentUser.HasAccessToSensitiveData() == false) + { + var sensitiveProperties = contentItem.PersistedContent.ContentType + .PropertyTypes.Where(x => contentItem.PersistedContent.ContentType.IsSensitiveProperty(x.Alias)) + .ToList(); + + foreach (var sensitiveProperty in sensitiveProperties) + { + //if found, change the value of the contentItem model to the persisted value so it remains unchanged + switch (sensitiveProperty.Alias) + { + case Constants.Conventions.Member.Comments: + contentItem.Comments = contentItem.PersistedContent.Comments; + break; + case Constants.Conventions.Member.IsApproved: + contentItem.IsApproved = contentItem.PersistedContent.IsApproved; + break; + case Constants.Conventions.Member.IsLockedOut: + contentItem.IsLockedOut = contentItem.PersistedContent.IsLockedOut; + break; + } + } + } + //Update the membership user if it has changed try { @@ -527,7 +562,7 @@ namespace Umbraco.Web.Editors var builtInAliases = Constants.Conventions.Member.GetStandardPropertyTypeStubs().Select(x => x.Key).ToArray(); foreach (var p in contentItem.PersistedContent.Properties) { - var valueMapped = currProps.SingleOrDefault(x => x.Alias == p.Alias); + var valueMapped = currProps.FirstOrDefault(x => x.Alias == p.Alias); if (builtInAliases.Contains(p.Alias) == false && valueMapped != null) { p.SetValue(valueMapped.GetValue()); @@ -747,5 +782,38 @@ namespace Umbraco.Web.Editors return Request.CreateResponse(HttpStatusCode.OK); } + + /// + /// Exports member data based on their unique Id + /// + /// The unique member identifier + /// + [HttpGet] + public HttpResponseMessage ExportMemberData(Guid key) + { + var currentUser = Security.CurrentUser; + + var httpResponseMessage = Request.CreateResponse(); + if (currentUser.HasAccessToSensitiveData() == false) + { + httpResponseMessage.StatusCode = HttpStatusCode.Forbidden; + return httpResponseMessage; + } + + var member = ((MemberService)Services.MemberService).ExportMember(key); + + var fileName = $"{member.Name}_{member.Email}.txt"; + + httpResponseMessage.Content = new ObjectContent(member, new JsonMediaTypeFormatter { Indent = true }); + httpResponseMessage.Content.Headers.Add("x-filename", fileName); + httpResponseMessage.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); + httpResponseMessage.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment"); + httpResponseMessage.Content.Headers.ContentDisposition.FileName = fileName; + httpResponseMessage.StatusCode = HttpStatusCode.OK; + + return httpResponseMessage; + } } + + } diff --git a/src/Umbraco.Web/Editors/MemberTypeController.cs b/src/Umbraco.Web/Editors/MemberTypeController.cs index 88a529d3e5..9ff0e33367 100644 --- a/src/Umbraco.Web/Editors/MemberTypeController.cs +++ b/src/Umbraco.Web/Editors/MemberTypeController.cs @@ -108,9 +108,47 @@ namespace Umbraco.Web.Editors public MemberTypeDisplay PostSave(MemberTypeSave contentTypeSave) { + //get the persisted member type + var ctId = Convert.ToInt32(contentTypeSave.Id); + var ct = ctId > 0 ? Services.MemberTypeService.Get(ctId) : null; + + if (UmbracoContext.Security.CurrentUser.HasAccessToSensitiveData() == false) + { + //We need to validate if any properties on the contentTypeSave have had their IsSensitiveValue changed, + //and if so, we need to check if the current user has access to sensitive values. If not, we have to return an error + var props = contentTypeSave.Groups.SelectMany(x => x.Properties); + if (ct != null) + { + foreach (var prop in props) + { + // Id 0 means the property was just added, no need to look it up + if (prop.Id == 0) + continue; + + var foundOnContentType = ct.PropertyTypes.FirstOrDefault(x => x.Id == prop.Id); + if (foundOnContentType == null) + throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.NotFound, "No property type with id " + prop.Id + " found on the content type")); + if (ct.IsSensitiveProperty(foundOnContentType.Alias) && prop.IsSensitiveData == false) + { + //if these don't match, then we cannot continue, this user is not allowed to change this value + throw new HttpResponseException(HttpStatusCode.Forbidden); + } + } + } + else + { + //if it is new, then we can just verify if any property has sensitive data turned on which is not allowed + if (props.Any(prop => prop.IsSensitiveData)) + { + throw new HttpResponseException(HttpStatusCode.Forbidden); + } + } + } + + var savedCt = PerformPostSave( contentTypeSave: contentTypeSave, - getContentType: i => Services.MemberTypeService.Get(i), + getContentType: i => ct, saveContentType: type => Services.MemberTypeService.Save(type)); var display = Mapper.Map(savedCt); diff --git a/src/Umbraco.Web/Editors/PasswordChanger.cs b/src/Umbraco.Web/Editors/PasswordChanger.cs index 7b99023ec4..1343d7cacd 100644 --- a/src/Umbraco.Web/Editors/PasswordChanger.cs +++ b/src/Umbraco.Web/Editors/PasswordChanger.cs @@ -88,7 +88,7 @@ namespace Umbraco.Web.Editors ? userMgr.GeneratePassword() : passwordModel.NewPassword; - var resetResult = await userMgr.ResetPasswordAsync(savingUser.Id, resetToken, newPass); + var resetResult = await userMgr.ChangePasswordWithResetAsync(savingUser.Id, resetToken, newPass); if (resetResult.Succeeded == false) { @@ -161,6 +161,7 @@ namespace Umbraco.Web.Editors } //Are we resetting the password?? + //TODO: I don't think this is required anymore since from 7.7 we no longer display the reset password checkbox since that didn't make sense. if (passwordModel.Reset.HasValue && passwordModel.Reset.Value) { var canReset = membershipProvider.CanResetPassword(_userService); diff --git a/src/Umbraco.Web/Editors/SectionController.cs b/src/Umbraco.Web/Editors/SectionController.cs index 4ee1efc8ae..76821cfc33 100644 --- a/src/Umbraco.Web/Editors/SectionController.cs +++ b/src/Umbraco.Web/Editors/SectionController.cs @@ -50,7 +50,7 @@ namespace Umbraco.Web.Editors { //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 + section.RoutePath = sectionTrees.IsContainer == false || sectionTrees.Children.Count == 0 ? sectionTrees.RoutePath : sectionTrees.Children[0].RoutePath; } diff --git a/src/Umbraco.Web/Editors/TourController.cs b/src/Umbraco.Web/Editors/TourController.cs new file mode 100644 index 0000000000..da16659cfe --- /dev/null +++ b/src/Umbraco.Web/Editors/TourController.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Newtonsoft.Json; +using Umbraco.Core.Configuration; +using Umbraco.Core.IO; +using Umbraco.Web.Models; +using Umbraco.Web.Mvc; + +namespace Umbraco.Web.Editors +{ + [PluginController("UmbracoApi")] + public class TourController : UmbracoAuthorizedJsonController + { + public IEnumerable GetTours() + { + var result = new List(); + + if (UmbracoConfig.For.UmbracoSettings().BackOffice.Tours.EnableTours == false) + return result; + + var filters = TourFilterResolver.Current.Filters.ToList(); + + //get all filters that will be applied to all tour aliases + var aliasOnlyFilters = filters.Where(x => x.PluginName == null && x.TourFileName == null).ToList(); + + //don't pass in any filters for core tours that have a plugin name assigned + var nonPluginFilters = filters.Where(x => x.PluginName == null).ToList(); + + //add core tour files + var coreToursPath = Path.Combine(IOHelper.MapPath(SystemDirectories.Config), "BackOfficeTours"); + if (Directory.Exists(coreToursPath)) + { + foreach (var tourFile in Directory.EnumerateFiles(coreToursPath, "*.json")) + { + TryParseTourFile(tourFile, result, nonPluginFilters, aliasOnlyFilters); + } + } + + //collect all tour files in packages + foreach (var plugin in Directory.EnumerateDirectories(IOHelper.MapPath(SystemDirectories.AppPlugins))) + { + var pluginName = Path.GetFileName(plugin.TrimEnd('\\')); + var pluginFilters = filters.Where(x => x.PluginName != null && x.PluginName.IsMatch(pluginName)).ToList(); + + //If there is any filter applied to match the plugin only (no file or tour alias) then ignore the plugin entirely + var isPluginFiltered = pluginFilters.Any(x => x.TourFileName == null && x.TourAlias == null); + if (isPluginFiltered) continue; + + //combine matched package filters with filters not specific to a package + var combinedFilters = nonPluginFilters.Concat(pluginFilters).ToList(); + + foreach (var backofficeDir in Directory.EnumerateDirectories(plugin, "backoffice")) + { + foreach (var tourDir in Directory.EnumerateDirectories(backofficeDir, "tours")) + { + foreach (var tourFile in Directory.EnumerateFiles(tourDir, "*.json")) + { + TryParseTourFile(tourFile, result, combinedFilters, aliasOnlyFilters, pluginName); + } + } + } + } + //Get all allowed sections for the current user + var allowedSections = UmbracoContext.Current.Security.CurrentUser.AllowedSections.ToList(); + + var toursToBeRemoved = new List(); + + //Checking to see if the user has access to the required tour sections, else we remove the tour + foreach (var backOfficeTourFile in result) + { + foreach (var tour in backOfficeTourFile.Tours) + { + foreach (var toursRequiredSection in tour.RequiredSections) + { + if (allowedSections.Contains(toursRequiredSection) == false) + { + toursToBeRemoved.Add(backOfficeTourFile); + break; + } + } + } + } + + return result.Except(toursToBeRemoved).OrderBy(x => x.FileName, StringComparer.InvariantCultureIgnoreCase); + } + + private void TryParseTourFile(string tourFile, + ICollection result, + List filters, + List aliasOnlyFilters, + string pluginName = null) + { + var fileName = Path.GetFileNameWithoutExtension(tourFile); + if (fileName == null) return; + + //get the filters specific to this file + var fileFilters = filters.Where(x => x.TourFileName != null && x.TourFileName.IsMatch(fileName)).ToList(); + + //If there is any filter applied to match the file only (no tour alias) then ignore the file entirely + var isFileFiltered = fileFilters.Any(x => x.TourAlias == null); + if (isFileFiltered) return; + + //now combine all aliases to filter below + var aliasFilters = aliasOnlyFilters.Concat(filters.Where(x => x.TourAlias != null)) + .Select(x => x.TourAlias) + .ToList(); + + try + { + var contents = File.ReadAllText(tourFile); + var tours = JsonConvert.DeserializeObject(contents); + + var tour = new BackOfficeTourFile + { + FileName = Path.GetFileNameWithoutExtension(tourFile), + PluginName = pluginName, + Tours = tours + .Where(x => aliasFilters.Count == 0 || aliasFilters.All(filter => filter.IsMatch(x.Alias)) == false) + .ToArray() + }; + + //don't add if all of the tours are filtered + if (tour.Tours.Any()) + result.Add(tour); + } + catch (IOException e) + { + throw new IOException("Error while trying to read file: " + tourFile, e); + } + catch (JsonReaderException e) + { + throw new JsonReaderException("Error while trying to parse content as tour data: " + tourFile, e); + } + } + } +} diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs index 3bb1e60be2..56bb0178bb 100644 --- a/src/Umbraco.Web/Editors/UsersController.cs +++ b/src/Umbraco.Web/Editors/UsersController.cs @@ -48,7 +48,7 @@ namespace Umbraco.Web.Editors /// public string[] GetCurrentUserAvatarUrls() { - var urls = UmbracoContext.Security.CurrentUser.GetCurrentUserAvatarUrls(Services.UserService, ApplicationCache.StaticCache); + var urls = UmbracoContext.Security.CurrentUser.GetUserAvatarUrls(ApplicationCache.StaticCache); if (urls == null) throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, "Could not access Gravatar endpoint")); @@ -116,7 +116,7 @@ namespace Umbraco.Web.Editors }); } - return request.CreateResponse(HttpStatusCode.OK, user.GetCurrentUserAvatarUrls(userService, staticCache)); + return request.CreateResponse(HttpStatusCode.OK, user.GetUserAvatarUrls(staticCache)); } [AppendUserModifiedHeader("id")] @@ -149,7 +149,7 @@ namespace Umbraco.Web.Editors Current.FileSystems.MediaFileSystem.DeleteFile(filePath); } - return Request.CreateResponse(HttpStatusCode.OK, found.GetCurrentUserAvatarUrls(Services.UserService, ApplicationCache.StaticCache)); + return Request.CreateResponse(HttpStatusCode.OK, found.GetUserAvatarUrls(ApplicationCache.StaticCache)); } /// @@ -157,6 +157,7 @@ namespace Umbraco.Web.Editors /// /// /// + [OutgoingEditorModelEvent] public UserDisplay GetById(int id) { var user = Services.UserService.GetUserById(id); @@ -384,7 +385,7 @@ namespace Umbraco.Web.Editors //send the email - await SendUserInviteEmailAsync(display, Security.CurrentUser.Name, user, userSave.Message); + await SendUserInviteEmailAsync(display, Security.CurrentUser.Name, Security.CurrentUser.Email, user, userSave.Message); return display; } @@ -421,7 +422,7 @@ namespace Umbraco.Web.Editors return attempt.Result; } - private async Task SendUserInviteEmailAsync(UserBasic userDisplay, string from, IUser to, string message) + private async Task SendUserInviteEmailAsync(UserBasic userDisplay, string from, string fromEmail, IUser to, string message) { var token = await UserManager.GenerateEmailConfirmationTokenAsync((int)userDisplay.Id); @@ -450,7 +451,7 @@ namespace Umbraco.Web.Editors var emailBody = Services.TextService.Localize("user/inviteEmailCopyFormat", //Ensure the culture of the found user is used for the email! UserExtensions.GetUserCulture(to.Language, Services.TextService), - new[] { userDisplay.Name, from, message, inviteUri.ToString() }); + new[] { userDisplay.Name, from, message, inviteUri.ToString(), fromEmail }); await UserManager.EmailService.SendAsync( //send the special UmbracoEmailMessage which configures it's own sender @@ -469,6 +470,7 @@ namespace Umbraco.Web.Editors /// /// /// + [OutgoingEditorModelEvent] public async Task PostSaveUser(UserSave userSave) { if (userSave == null) throw new ArgumentNullException("userSave"); @@ -525,7 +527,7 @@ namespace Umbraco.Web.Editors // if the found user has his email for username, we want to keep this synced when changing the email. // we have already cross-checked above that the email isn't colliding with anything, so we can safely assign it here. - if (found.Username == found.Email && userSave.Username != userSave.Email) + if (UmbracoConfig.For.UmbracoSettings().Security.UsernameIsEmail && found.Username == found.Email && userSave.Username != userSave.Email) { userSave.Username = userSave.Email; } @@ -534,19 +536,11 @@ namespace Umbraco.Web.Editors { var passwordChanger = new PasswordChanger(Logger, Services.UserService, UmbracoContext.HttpContext); + //this will change the password and raise appropriate events var passwordChangeResult = await passwordChanger.ChangePasswordWithIdentityAsync(Security.CurrentUser, found, userSave.ChangePassword, UserManager); if (passwordChangeResult.Success) { - var userMgr = this.TryGetOwinContext().Result.GetBackOfficeUserManager(); - - //raise the event - NOTE that the ChangePassword.Reset value here doesn't mean it's been 'reset', it means - //it's been changed by a back office user - if (userSave.ChangePassword.Reset.HasValue && userSave.ChangePassword.Reset.Value) - { - userMgr.RaisePasswordChangedEvent(intId.Result); - } - - //need to re-get the user + //need to re-get the user found = Services.UserService.GetUserById(intId.Result); } else diff --git a/src/Umbraco.Web/ExamineStartup.cs b/src/Umbraco.Web/ExamineStartup.cs new file mode 100644 index 0000000000..f3a8bc3ef5 --- /dev/null +++ b/src/Umbraco.Web/ExamineStartup.cs @@ -0,0 +1,212 @@ +// fixme - Examine is borked +// this cannot compile because it seems to require some changes that are not in Examine v2 +// this should *not* exist, see ExamineComponent instead, which handles everything about Examine + +//using System; +//using System.Collections.Generic; +//using System.Linq; +//using Examine; +//using Examine.Config; +//using Examine.LuceneEngine; +//using Examine.LuceneEngine.Providers; +//using Examine.Providers; +//using Lucene.Net.Index; +//using Lucene.Net.Store; +//using Umbraco.Core; +//using Umbraco.Core.Logging; +//using UmbracoExamine; + +//namespace Umbraco.Web +//{ +// /// +// /// Used to configure Examine during startup for the web application +// /// +// internal class ExamineStartup +// { +// private readonly List _indexesToRebuild = new List(); +// private readonly ApplicationContext _appCtx; +// private readonly ProfilingLogger _profilingLogger; +// private static bool _isConfigured = false; + +// //this is used if we are not the MainDom, in which case we need to ensure that if indexes need rebuilding that this +// //doesn't occur since that should only occur when we are MainDom +// private bool _disableExamineIndexing = false; + +// public ExamineStartup(ApplicationContext appCtx) +// { +// _appCtx = appCtx; +// _profilingLogger = appCtx.ProfilingLogger; +// } + +// /// +// /// Called during the initialize operation of the boot manager process +// /// +// public void Initialize() +// { +// //We want to manage Examine's appdomain shutdown sequence ourselves so first we'll disable Examine's default behavior +// //and then we'll use MainDom to control Examine's shutdown +// ExamineManager.DisableDefaultHostingEnvironmentRegistration(); + +// //we want to tell examine to use a different fs lock instead of the default NativeFSFileLock which could cause problems if the appdomain +// //terminates and in some rare cases would only allow unlocking of the file if IIS is forcefully terminated. Instead we'll rely on the simplefslock +// //which simply checks the existence of the lock file +// DirectoryTracker.DefaultLockFactory = d => +// { +// var simpleFsLockFactory = new NoPrefixSimpleFsLockFactory(d); +// return simpleFsLockFactory; +// }; + +// //This is basically a hack for this item: http://issues.umbraco.org/issue/U4-5976 +// // when Examine initializes it will try to rebuild if the indexes are empty, however in many cases not all of Examine's +// // event handlers will be assigned during bootup when the rebuilding starts which is a problem. So with the examine 0.1.58.2941 build +// // it has an event we can subscribe to in order to cancel this rebuilding process, but what we'll do is cancel it and postpone the rebuilding until the +// // boot process has completed. It's a hack but it works. +// ExamineManager.Instance.BuildingEmptyIndexOnStartup += OnInstanceOnBuildingEmptyIndexOnStartup; + +// //let's deal with shutting down Examine with MainDom +// var examineShutdownRegistered = _appCtx.MainDom.Register(() => +// { +// using (_profilingLogger.TraceDuration("Examine shutting down")) +// { +// //Due to the way Examine's own IRegisteredObject works, we'll first run it with immediate=false and then true so that +// //it's correct subroutines are executed (otherwise we'd have to run this logic manually ourselves) +// ExamineManager.Instance.Stop(false); +// ExamineManager.Instance.Stop(true); +// } +// }); +// if (examineShutdownRegistered) +// { +// _profilingLogger.Logger.Debug("Examine shutdown registered with MainDom"); +// } +// else +// { +// _profilingLogger.Logger.Debug("Examine shutdown not registered, this appdomain is not the MainDom"); + +// //if we could not register the shutdown examine ourselves, it means we are not maindom! in this case all of examine should be disabled +// //from indexing anything on startup!! +// _disableExamineIndexing = true; +// Suspendable.ExamineEvents.SuspendIndexers(); +// } +// } + +// /// +// /// Called during the Complete operation of the boot manager process +// /// +// public void Complete() +// { +// EnsureUnlockedAndConfigured(); + +// //Ok, now that everything is complete we'll check if we've stored any references to index that need rebuilding and run them +// // (see the initialize method for notes) - we'll ensure we remove the event handler too in case examine manager doesn't actually +// // initialize during startup, in which case we want it to rebuild the indexes itself. +// ExamineManager.Instance.BuildingEmptyIndexOnStartup -= OnInstanceOnBuildingEmptyIndexOnStartup; + +// //don't do anything if we have disabled this +// if (_disableExamineIndexing == false) +// { +// foreach (var indexer in _indexesToRebuild) +// { +// indexer.RebuildIndex(); +// } +// } +// } + +// /// +// /// Called to perform the rebuilding indexes on startup if the indexes don't exist +// /// +// public void RebuildIndexes() +// { +// //don't do anything if we have disabled this +// if (_disableExamineIndexing) return; + +// EnsureUnlockedAndConfigured(); + +// //If the developer has explicitly opted out of rebuilding indexes on startup then we +// // should adhere to that and not do it, this means that if they are load balancing things will be +// // out of sync if they are auto-scaling but there's not much we can do about that. +// if (ExamineSettings.Instance.RebuildOnAppStart == false) return; + +// foreach (var indexer in GetIndexesForColdBoot()) +// { +// indexer.RebuildIndex(); +// } +// } + +// /// +// /// The method used to create indexes on a cold boot +// /// +// /// +// /// A cold boot is when the server determines it will not (or cannot) process instructions in the cache table and +// /// will rebuild it's own caches itself. +// /// +// public IEnumerable GetIndexesForColdBoot() +// { +// // NOTE: This is IMPORTANT! ... we don't want to rebuild any index that is already flagged to be re-indexed +// // on startup based on our _indexesToRebuild variable and how Examine auto-rebuilds when indexes are empty. +// // This callback is used above for the DatabaseServerMessenger startup options. + +// // all indexes +// IEnumerable indexes = ExamineManager.Instance.IndexProviderCollection; + +// // except those that are already flagged +// // and are processed in Complete() +// if (_indexesToRebuild.Any()) +// indexes = indexes.Except(_indexesToRebuild); + +// // return +// foreach (var index in indexes) +// yield return index; +// } + +// /// +// /// Must be called to configure each index and ensure it's unlocked before any indexing occurs +// /// +// /// +// /// Indexing rebuilding can occur on a normal boot if the indexes are empty or on a cold boot by the database server messenger. Before +// /// either of these happens, we need to configure the indexes. +// /// +// private void EnsureUnlockedAndConfigured() +// { +// if (_isConfigured) return; + +// _isConfigured = true; + +// foreach (var luceneIndexer in ExamineManager.Instance.IndexProviderCollection.OfType()) +// { +// //We now need to disable waiting for indexing for Examine so that the appdomain is shutdown immediately and doesn't wait for pending +// //indexing operations. We used to wait for indexing operations to complete but this can cause more problems than that is worth because +// //that could end up halting shutdown for a very long time causing overlapping appdomains and many other problems. +// luceneIndexer.WaitForIndexQueueOnShutdown = false; + +// //we should check if the index is locked ... it shouldn't be! We are using simple fs lock now and we are also ensuring that +// //the indexes are not operational unless MainDom is true so if _disableExamineIndexing is false then we should be in charge +// if (_disableExamineIndexing == false) +// { +// var dir = luceneIndexer.GetLuceneDirectory(); +// if (IndexWriter.IsLocked(dir)) +// { +// _profilingLogger.Logger.Info("Forcing index " + luceneIndexer.IndexSetName + " to be unlocked since it was left in a locked state"); +// IndexWriter.Unlock(dir); +// } +// } +// } +// } + +// private void OnInstanceOnBuildingEmptyIndexOnStartup(object sender, BuildingEmptyIndexOnStartupEventArgs args) +// { +// //store the indexer that needs rebuilding because it's empty for when the boot process +// // is complete and cancel this current event so the rebuild process doesn't start right now. +// args.Cancel = true; +// _indexesToRebuild.Add((BaseIndexProvider)args.Indexer); + +// //check if the index is rebuilding due to an error and log it +// if (args.IsHealthy == false) +// { +// var baseIndex = args.Indexer as BaseIndexProvider; +// var name = baseIndex != null ? baseIndex.Name : "[UKNOWN]"; + +// _profilingLogger.Logger.Error(string.Format("The index {0} is rebuilding due to being unreadable/corrupt", name), args.UnhealthyException); +// } +// } +// } +//} diff --git a/src/Umbraco.Web/HealthCheck/Checks/Config/AbstractConfigCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Config/AbstractConfigCheck.cs index 42cb575491..01863dac0c 100644 --- a/src/Umbraco.Web/HealthCheck/Checks/Config/AbstractConfigCheck.cs +++ b/src/Umbraco.Web/HealthCheck/Checks/Config/AbstractConfigCheck.cs @@ -128,7 +128,7 @@ namespace Umbraco.Web.HealthCheck.Checks.Config public override IEnumerable GetStatus() { - var successMessage = string.Format(CheckSuccessMessage, FileName, XPath, Values, CurrentValue); + var successMessage = string.Format(CheckSuccessMessage, FileName, XPath, Values); var configValue = _configurationService.GetConfigurationValue(); if (configValue.Success == false) @@ -144,6 +144,9 @@ namespace Umbraco.Web.HealthCheck.Checks.Config CurrentValue = configValue.Result; + // need to update the successMessage with the CurrentValue + successMessage = string.Format(CheckSuccessMessage, FileName, XPath, Values, CurrentValue); + var valueFound = Values.Any(value => string.Equals(CurrentValue, value.Value, StringComparison.InvariantCultureIgnoreCase)); if (ValueComparisonType == ValueComparisonType.ShouldEqual && valueFound || ValueComparisonType == ValueComparisonType.ShouldNotEqual && valueFound == false) { diff --git a/src/Umbraco.Web/HealthCheck/HealthCheckController.cs b/src/Umbraco.Web/HealthCheck/HealthCheckController.cs index d051bcd6a3..14642ac843 100644 --- a/src/Umbraco.Web/HealthCheck/HealthCheckController.cs +++ b/src/Umbraco.Web/HealthCheck/HealthCheckController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Configuration; using System.Linq; using System.Web.Http; @@ -7,12 +8,14 @@ using Umbraco.Core.Logging; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.HealthChecks; using Umbraco.Web.Editors; +using Umbraco.Web.WebApi.Filters; namespace Umbraco.Web.HealthCheck { /// /// The API controller used to display the health check info and execute any actions /// + [UmbracoApplicationAuthorize(Core.Constants.Applications.Developer)] public class HealthCheckController : UmbracoAuthorizedJsonController { private readonly HealthCheckCollection _checks; diff --git a/src/Umbraco.Web/HtmlStringUtilities.cs b/src/Umbraco.Web/HtmlStringUtilities.cs index 17ee58ef93..26ee4c6557 100644 --- a/src/Umbraco.Web/HtmlStringUtilities.cs +++ b/src/Umbraco.Web/HtmlStringUtilities.cs @@ -96,6 +96,8 @@ namespace Umbraco.Web using (var outputms = new MemoryStream()) { + bool lengthReached = false; + using (var outputtw = new StreamWriter(outputms)) { using (var ms = new MemoryStream()) @@ -110,7 +112,6 @@ namespace Umbraco.Web using (TextReader tr = new StreamReader(ms)) { bool isInsideElement = false, - lengthReached = false, insideTagSpaceEncountered = false, isTagClose = false; @@ -258,10 +259,15 @@ namespace Umbraco.Web //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) + if (addElipsis && lengthReached && string.IsNullOrWhiteSpace(firstTrim) == false) { result = firstTrim[firstTrim.Length - hellip.Length - 1] == ' ' ? firstTrim.Remove(firstTrim.Length - hellip.Length - 1, 1) : firstTrim; } + else + { + result = firstTrim; + } + return new HtmlString(result); } } diff --git a/src/Umbraco.Web/IPublishedContentQuery.cs b/src/Umbraco.Web/IPublishedContentQuery.cs index 2938fa36d9..ddaae9922d 100644 --- a/src/Umbraco.Web/IPublishedContentQuery.cs +++ b/src/Umbraco.Web/IPublishedContentQuery.cs @@ -32,20 +32,23 @@ namespace Umbraco.Web IEnumerable MediaAtRoot(); /// - /// Searches content + /// Searches content. /// - /// - /// - /// - /// IEnumerable Search(string term, bool useWildCards = true, string searchProvider = null); /// - /// Searhes content + /// Searches content. + /// + IEnumerable Search(int skip, int take, out int totalRecords, string term, bool useWildCards = true, string searchProvider = null); + + /// + /// Searches content. /// - /// - /// - /// IEnumerable Search(Examine.SearchCriteria.ISearchCriteria criteria, Examine.Providers.BaseSearchProvider searchProvider = null); + + /// + /// Searches content. + /// + IEnumerable Search(int skip, int take, out int totalrecords, Examine.SearchCriteria.ISearchCriteria criteria, Examine.Providers.BaseSearchProvider searchProvider = null); } } diff --git a/src/Umbraco.Web/Install/Controllers/InstallController.cs b/src/Umbraco.Web/Install/Controllers/InstallController.cs index 2820455c70..7cf12b9f4c 100644 --- a/src/Umbraco.Web/Install/Controllers/InstallController.cs +++ b/src/Umbraco.Web/Install/Controllers/InstallController.cs @@ -1,4 +1,5 @@ -using System.Web.Mvc; +using System; +using System.Web.Mvc; using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.IO; @@ -40,7 +41,8 @@ namespace Umbraco.Web.Install.Controllers { // Update ClientDependency version var clientDependencyConfig = new ClientDependencyConfiguration(_logger); - var clientDependencyUpdated = clientDependencyConfig.IncreaseVersionNumber(); + var clientDependencyUpdated = clientDependencyConfig.UpdateVersionNumber( + UmbracoVersion.SemanticVersion, DateTime.UtcNow, "yyyyMMdd"); // Delete ClientDependency temp directories to make sure we get fresh caches var clientDependencyTempFilesDeleted = clientDependencyConfig.ClearTempFiles(HttpContext); diff --git a/src/Umbraco.Web/Install/InstallHelper.cs b/src/Umbraco.Web/Install/InstallHelper.cs index a0c86f066c..31f79fc0a3 100644 --- a/src/Umbraco.Web/Install/InstallHelper.cs +++ b/src/Umbraco.Web/Install/InstallHelper.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using System.Net.Http; using System.Web; +using Semver; using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.IO; @@ -45,7 +46,7 @@ namespace Umbraco.Web.Install { // fixme - should NOT use current everywhere here - inject! new NewInstallStep(_httpContext, Current.Services.UserService, _databaseBuilder), - new UpgradeStep(_databaseBuilder), + new UpgradeStep(), new FilePermissionsStep(), new MajorVersion7UpgradeReport(_databaseBuilder, Current.RuntimeState, Current.SqlContext, Current.ScopeProvider), new Version73FileCleanup(_httpContext, _logger), diff --git a/src/Umbraco.Web/Install/InstallSteps/UpgradeStep.cs b/src/Umbraco.Web/Install/InstallSteps/UpgradeStep.cs index 5bf527d46d..75b027f69f 100644 --- a/src/Umbraco.Web/Install/InstallSteps/UpgradeStep.cs +++ b/src/Umbraco.Web/Install/InstallSteps/UpgradeStep.cs @@ -1,9 +1,4 @@ -using System; -using Semver; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.Migrations.Install; -using Umbraco.Web.Composing; +using Umbraco.Core.Configuration; using Umbraco.Web.Install.Models; namespace Umbraco.Web.Install.InstallSteps @@ -11,17 +6,9 @@ namespace Umbraco.Web.Install.InstallSteps /// /// This step is purely here to show the button to commence the upgrade /// - [InstallSetupStep(InstallationType.Upgrade, - "Upgrade", "upgrade", 1, "Upgrading Umbraco to the latest and greatest version.")] + [InstallSetupStep(InstallationType.Upgrade, "Upgrade", "upgrade", 1, "Upgrading Umbraco to the latest and greatest version.")] internal class UpgradeStep : InstallSetupStep { - private readonly DatabaseBuilder _databaseBuilder; - - public UpgradeStep(DatabaseBuilder databaseBuilder) - { - _databaseBuilder = databaseBuilder; - } - public override bool RequiresExecution(object model) { return true; @@ -36,62 +23,13 @@ namespace Umbraco.Web.Install.InstallSteps { get { - var currentVersion = CurrentVersion().GetVersion(3).ToString(); + // fixme where is the "detected current version"? + var currentVersion = UmbracoVersion.Local.ToString(); var newVersion = UmbracoVersion.Current.ToString(); - var reportUrl = string.Format("https://our.umbraco.org/contribute/releases/compare?from={0}&to={1}¬es=1", currentVersion, newVersion); - - return new - { - currentVersion = currentVersion, - newVersion = newVersion, - reportUrl = reportUrl - }; + var reportUrl = $"https://our.umbraco.org/contribute/releases/compare?from={currentVersion}&to={newVersion}¬es=1"; + return new { currentVersion, newVersion, reportUrl }; } } - - /// - /// Gets the Current Version of the Umbraco Site before an upgrade - /// by using the last/most recent Umbraco Migration that has been run - /// - /// A SemVersion of the latest Umbraco DB Migration run - private SemVersion CurrentVersion() - { - // start with a default version of 0.0.0 - var version = new SemVersion(0); - - //If we have a db context available, if we don't then we are not installed anyways - // fixme DB will not tell the version anymore - only the upgrade state - //if (_databaseBuilder.IsDatabaseConfigured && _databaseBuilder.CanConnect) - // version = _databaseBuilder.ValidateDatabaseSchema().DetermineInstalledVersionByMigrations(Current.Services.MigrationEntryService); - - if (version != new SemVersion(0)) - return version; - - // If we aren't able to get a result from the umbracoMigrations table then use the version in web.config, if it's available - if (string.IsNullOrWhiteSpace(GlobalSettings.ConfigurationStatus)) - return version; - - var configuredVersion = GlobalSettings.ConfigurationStatus; - - string currentComment = null; - - var current = configuredVersion.Split('-'); - if (current.Length > 1) - currentComment = current[1]; - - Version currentVersion; - if (Version.TryParse(current[0], out currentVersion)) - { - version = new SemVersion( - currentVersion.Major, - currentVersion.Minor, - currentVersion.Build, - string.IsNullOrWhiteSpace(currentComment) ? null : currentComment, - currentVersion.Revision > 0 ? currentVersion.Revision.ToString() : null); - } - - return version; - } } } diff --git a/src/Umbraco.Web/Models/BackOfficeTour.cs b/src/Umbraco.Web/Models/BackOfficeTour.cs new file mode 100644 index 0000000000..78a4cd1897 --- /dev/null +++ b/src/Umbraco.Web/Models/BackOfficeTour.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models +{ + /// + /// A model representing a tour. + /// + [DataContract(Name = "tour", Namespace = "")] + public class BackOfficeTour + { + [DataMember(Name = "name")] + public string Name { get; set; } + [DataMember(Name = "alias")] + public string Alias { get; set; } + [DataMember(Name = "group")] + public string Group { get; set; } + [DataMember(Name = "groupOrder")] + public int GroupOrder { get; set; } + [DataMember(Name = "allowDisable")] + public bool AllowDisable { get; set; } + [DataMember(Name = "requiredSections")] + public List RequiredSections { get; set; } + [DataMember(Name = "steps")] + public BackOfficeTourStep[] Steps { get; set; } + } +} diff --git a/src/Umbraco.Web/Models/BackOfficeTourFile.cs b/src/Umbraco.Web/Models/BackOfficeTourFile.cs new file mode 100644 index 0000000000..7291a89ff4 --- /dev/null +++ b/src/Umbraco.Web/Models/BackOfficeTourFile.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models +{ + /// + /// A model representing the file used to load a tour. + /// + [DataContract(Name = "tourFile", Namespace = "")] + public class BackOfficeTourFile + { + /// + /// The file name for the tour + /// + [DataMember(Name = "fileName")] + public string FileName { get; set; } + + /// + /// The plugin folder that the tour comes from + /// + /// + /// If this is null it means it's a Core tour + /// + [DataMember(Name = "pluginName")] + public string PluginName { get; set; } + + [DataMember(Name = "tours")] + public IEnumerable Tours { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/BackOfficeTourFilter.cs b/src/Umbraco.Web/Models/BackOfficeTourFilter.cs new file mode 100644 index 0000000000..994cdb6d29 --- /dev/null +++ b/src/Umbraco.Web/Models/BackOfficeTourFilter.cs @@ -0,0 +1,61 @@ +using System.Text.RegularExpressions; + +namespace Umbraco.Web.Models +{ + public class BackOfficeTourFilter + { + public Regex PluginName { get; private set; } + public Regex TourFileName { get; private set; } + public Regex TourAlias { get; private set; } + + /// + /// Create a filter to filter out a whole plugin's tours + /// + /// + /// + public static BackOfficeTourFilter FilterPlugin(Regex pluginName) + { + return new BackOfficeTourFilter(pluginName, null, null); + } + + /// + /// Create a filter to filter out a whole tour file + /// + /// + /// + public static BackOfficeTourFilter FilterFile(Regex tourFileName) + { + return new BackOfficeTourFilter(null, tourFileName, null); + } + + /// + /// Create a filter to filter out a tour alias, this will filter out the same alias found in all files + /// + /// + /// + public static BackOfficeTourFilter FilterAlias(Regex tourAlias) + { + return new BackOfficeTourFilter(null, null, tourAlias); + } + + /// + /// Constructor to create a tour filter + /// + /// Value to filter out tours by a plugin, can be null + /// Value to filter out a tour file, can be null + /// Value to filter out a tour alias, can be null + /// + /// Depending on what is null will depend on how the filter is applied. + /// If pluginName is not NULL and it's matched then we check if tourFileName is not NULL and it's matched then we check tour alias is not NULL and then match it, + /// if any steps is NULL then the filters upstream are applied. + /// Example, pluginName = "hello", tourFileName="stuff", tourAlias=NULL = we will filter out the tour file "stuff" from the plugin "hello" but not from other plugins if the same file name exists. + /// Example, tourAlias="test.*" = we will filter out all tour aliases that start with the word "test" regardless of the plugin or file name + /// + public BackOfficeTourFilter(Regex pluginName, Regex tourFileName, Regex tourAlias) + { + PluginName = pluginName; + TourFileName = tourFileName; + TourAlias = tourAlias; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/BackOfficeTourStep.cs b/src/Umbraco.Web/Models/BackOfficeTourStep.cs new file mode 100644 index 0000000000..a64bf15b7f --- /dev/null +++ b/src/Umbraco.Web/Models/BackOfficeTourStep.cs @@ -0,0 +1,33 @@ +using System.Runtime.Serialization; +using Newtonsoft.Json.Linq; + +namespace Umbraco.Web.Models +{ + /// + /// A model representing a step in a tour. + /// + [DataContract(Name = "step", Namespace = "")] + public class BackOfficeTourStep + { + [DataMember(Name = "title")] + public string Title { get; set; } + [DataMember(Name = "content")] + public string Content { get; set; } + [DataMember(Name = "type")] + public string Type { get; set; } + [DataMember(Name = "element")] + public string Element { get; set; } + [DataMember(Name = "elementPreventClick")] + public bool ElementPreventClick { get; set; } + [DataMember(Name = "backdropOpacity")] + public float? BackdropOpacity { get; set; } + [DataMember(Name = "event")] + public string Event { get; set; } + [DataMember(Name = "view")] + public string View { get; set; } + [DataMember(Name = "eventElement")] + public string EventElement { get; set; } + [DataMember(Name = "customProperties")] + public JObject CustomProperties { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/AuditLog.cs b/src/Umbraco.Web/Models/ContentEditing/AuditLog.cs index 236ddec941..3b945f40d9 100644 --- a/src/Umbraco.Web/Models/ContentEditing/AuditLog.cs +++ b/src/Umbraco.Web/Models/ContentEditing/AuditLog.cs @@ -1,24 +1,33 @@ using System; using System.Runtime.Serialization; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Umbraco.Core.Models; namespace Umbraco.Web.Models.ContentEditing { [DataContract(Name = "auditLog", Namespace = "")] public class AuditLog { - [DataMember(Name = "userId", IsRequired = true)] + [DataMember(Name = "userId")] public int UserId { get; set; } - [DataMember(Name = "nodeId", IsRequired = true)] + [DataMember(Name = "userName")] + public string UserName { get; set; } + + [DataMember(Name = "userAvatars")] + public string[] UserAvatars { get; set; } + + [DataMember(Name = "nodeId")] public int NodeId { get; set; } - [DataMember(Name = "timestamp", IsRequired = true)] + [DataMember(Name = "timestamp")] public DateTime Timestamp { get; set; } - [DataMember(Name = "logType", IsRequired = true)] + [DataMember(Name = "logType")] public string LogType { get; set; } - [DataMember(Name = "comment", IsRequired = true)] + [DataMember(Name = "comment")] public string Comment { get; set; } } } diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs index e5562ba22d..cdf13fee7c 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs @@ -5,7 +5,6 @@ using Umbraco.Core.Models; namespace Umbraco.Web.Models.ContentEditing { - /// /// A model representing a content item to be displayed in the back office /// @@ -29,6 +28,12 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "template")] public string TemplateAlias { get; set; } + [DataMember(Name = "allowedTemplates")] + public IDictionary AllowedTemplates { get; set; } + + [DataMember(Name = "documentType")] + public ContentTypeBasic DocumentType { get; set; } + [DataMember(Name = "urls")] public string[] Urls { get; set; } diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentPropertyBasic.cs b/src/Umbraco.Web/Models/ContentEditing/ContentPropertyBasic.cs index 58c838382a..199b32da9b 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentPropertyBasic.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentPropertyBasic.cs @@ -30,6 +30,11 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "editor", IsRequired = false)] public string Editor { get; set; } + /// + /// Flags the property to denote that it can contain sensitive data + /// + [DataMember(Name = "isSensitive", IsRequired = false)] + public bool IsSensitive { get; set; } /// /// Used internally during model mapping diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentPropertyDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/ContentPropertyDisplay.cs index f7268b6c27..8a09c62333 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentPropertyDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentPropertyDisplay.cs @@ -35,5 +35,8 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "validation")] public PropertyTypeValidation Validation { get; set; } + + [DataMember(Name = "readonly")] + public bool Readonly { get; set; } } } diff --git a/src/Umbraco.Web/Models/ContentEditing/EditorNavigation.cs b/src/Umbraco.Web/Models/ContentEditing/EditorNavigation.cs new file mode 100644 index 0000000000..29922750cf --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/EditorNavigation.cs @@ -0,0 +1,26 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + /// + /// A model representing the navigation ("apps") inside an editor in the back office + /// + [DataContract(Name = "user", Namespace = "")] + public class EditorNavigation + { + [DataMember(Name = "name")] + public string Name { get; set; } + + [DataMember(Name = "alias")] + public string Alias { get; set; } + + [DataMember(Name = "icon")] + public string Icon { get; set; } + + [DataMember(Name = "view")] + public string View { get; set; } + + [DataMember(Name = "active")] + public bool Active { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/EntityBasic.cs b/src/Umbraco.Web/Models/ContentEditing/EntityBasic.cs index 471b81ea58..0c26332fa6 100644 --- a/src/Umbraco.Web/Models/ContentEditing/EntityBasic.cs +++ b/src/Umbraco.Web/Models/ContentEditing/EntityBasic.cs @@ -2,10 +2,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; -using System.Linq; using System.Runtime.Serialization; -using System.Text; -using System.Threading.Tasks; using Newtonsoft.Json; using Umbraco.Core; using Umbraco.Core.Models.Validation; diff --git a/src/Umbraco.Web/Models/ContentEditing/MediaItemDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/MediaItemDisplay.cs index 7d314c0392..069ae25547 100644 --- a/src/Umbraco.Web/Models/ContentEditing/MediaItemDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/MediaItemDisplay.cs @@ -10,6 +10,10 @@ namespace Umbraco.Web.Models.ContentEditing [DataContract(Name = "content", Namespace = "")] public class MediaItemDisplay : ListViewAwareContentItemDisplayBase { + [DataMember(Name = "contentType")] + public ContentTypeBasic ContentType { get; set; } + [DataMember(Name = "mediaLink")] + public string MediaLink { get; set; } } } diff --git a/src/Umbraco.Web/Models/ContentEditing/MemberPropertyTypeBasic.cs b/src/Umbraco.Web/Models/ContentEditing/MemberPropertyTypeBasic.cs index 6927cb44a0..f3fe7a262c 100644 --- a/src/Umbraco.Web/Models/ContentEditing/MemberPropertyTypeBasic.cs +++ b/src/Umbraco.Web/Models/ContentEditing/MemberPropertyTypeBasic.cs @@ -13,5 +13,8 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "memberCanEdit")] public bool MemberCanEditProperty { get; set; } + + [DataMember(Name = "isSensitiveData")] + public bool IsSensitiveData { get; set; } } } diff --git a/src/Umbraco.Web/Models/ContentEditing/MemberPropertyTypeDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/MemberPropertyTypeDisplay.cs index 0c045afbdd..e5612f5247 100644 --- a/src/Umbraco.Web/Models/ContentEditing/MemberPropertyTypeDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/MemberPropertyTypeDisplay.cs @@ -10,5 +10,8 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "memberCanEdit")] public bool MemberCanEditProperty { get; set; } + + [DataMember(Name = "isSensitiveData")] + public bool IsSensitiveData { get; set; } } } diff --git a/src/Umbraco.Web/Models/ContentEditing/PostedFolder.cs b/src/Umbraco.Web/Models/ContentEditing/PostedFolder.cs new file mode 100644 index 0000000000..35cd908787 --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/PostedFolder.cs @@ -0,0 +1,17 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + /// + /// Used to create a folder with the MediaController + /// + [DataContract] + public class PostedFolder + { + [DataMember(Name = "parentId")] + public string ParentId { get; set; } + + [DataMember(Name = "name")] + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs b/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs index 6b819325e5..dc240bfb78 100644 --- a/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs +++ b/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs @@ -65,7 +65,5 @@ namespace Umbraco.Web.Models.ContentEditing /// [DataMember(Name = "allowedSections")] public IEnumerable AllowedSections { get; set; } - - } } diff --git a/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs index 017f1ea218..35317d8dd5 100644 --- a/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs @@ -18,8 +18,13 @@ namespace Umbraco.Web.Models.ContentEditing AvailableCultures = new Dictionary(); StartContentIds = new List(); StartMediaIds = new List(); + Navigation = new List(); } + [DataMember(Name = "navigation")] + [ReadOnly(true)] + public IEnumerable Navigation { get; set; } + /// /// Gets the available cultures (i.e. to populate a drop down) /// The key is the culture stored in the database, the value is the Name diff --git a/src/Umbraco.Web/Models/Mapping/ActionButtonsResolver.cs b/src/Umbraco.Web/Models/Mapping/ActionButtonsResolver.cs index 609f4fa32a..59b594757f 100644 --- a/src/Umbraco.Web/Models/Mapping/ActionButtonsResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/ActionButtonsResolver.cs @@ -11,33 +11,34 @@ namespace Umbraco.Web.Models.Mapping /// internal class ActionButtonsResolver { - private readonly Lazy _userService; - - public ActionButtonsResolver(Lazy userService) + public ActionButtonsResolver(IUserService userService, IContentService contentService) { - _userService = userService; + UserService = userService; + ContentService = contentService; } + private IUserService UserService { get; } + private IContentService ContentService { get; } + public IEnumerable Resolve(IContent source) { + //cannot check permissions without a context if (UmbracoContext.Current == null) - { - //cannot check permissions without a context return Enumerable.Empty(); + + string path; + if (source.HasIdentity) + path = source.Path; + else + { + var parent = ContentService.GetById(source.ParentId); + path = parent == null ? "-1" : parent.Path; } - var svc = _userService.Value; - var permissions = svc.GetPermissions( - //TODO: This is certainly not ideal usage here - perhaps the best way to deal with this in the future is - // with the IUmbracoContextAccessor. In the meantime, if used outside of a web app this will throw a null - // refrence exception :( - UmbracoContext.Current.Security.CurrentUser, - // Here we need to do a special check since this could be new content, in which case we need to get the permissions - // from the parent, not the existing one otherwise permissions would be coming from the root since Id is 0. - source.HasIdentity ? source.Id : source.ParentId) - .GetAllPermissions(); - - return permissions; + //TODO: This is certainly not ideal usage here - perhaps the best way to deal with this in the future is + // with the IUmbracoContextAccessor. In the meantime, if used outside of a web app this will throw a null + // refrence exception :( + return UserService.GetPermissionsForPath(UmbracoContext.Current.Security.CurrentUser, path).GetAllPermissions(); } } } diff --git a/src/Umbraco.Web/Models/Mapping/AuditMapperProfile.cs b/src/Umbraco.Web/Models/Mapping/AuditMapperProfile.cs new file mode 100644 index 0000000000..34749c51c6 --- /dev/null +++ b/src/Umbraco.Web/Models/Mapping/AuditMapperProfile.cs @@ -0,0 +1,20 @@ +using AutoMapper; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.Web.Models.Mapping +{ + internal class AuditMapperProfile : Profile + { + public AuditMapperProfile() + { + CreateMap() + .ForMember(log => log.UserAvatars, expression => expression.Ignore()) + .ForMember(log => log.UserName, expression => expression.Ignore()) + .ForMember(log => log.NodeId, expression => expression.MapFrom(item => item.Id)) + .ForMember(log => log.Timestamp, expression => expression.MapFrom(item => item.CreateDate)) + .ForMember(log => log.LogType, expression => expression.MapFrom(item => item.AuditType)); + } + } +} diff --git a/src/Umbraco.Web/Models/Mapping/AutoMapperExtensions.cs b/src/Umbraco.Web/Models/Mapping/AutoMapperExtensions.cs new file mode 100644 index 0000000000..fcbb98a7cb --- /dev/null +++ b/src/Umbraco.Web/Models/Mapping/AutoMapperExtensions.cs @@ -0,0 +1,31 @@ +using System; +using AutoMapper; + +namespace Umbraco.Web.Models.Mapping +{ + /// + /// Extends AutoMapper's class to handle Umbraco's context. + /// + internal static class ContextMapper + { + private const string UmbracoContextKey = "ContextMapper.UmbracoContext"; + + public static TDestination Map(TSource obj, UmbracoContext umbracoContext) + => Mapper.Map(obj, opt => opt.Items[UmbracoContextKey] = umbracoContext); + + public static UmbracoContext GetUmbracoContext(this ResolutionContext resolutionContext, bool throwIfMissing = true) + { + if (resolutionContext.Options.Items.TryGetValue(UmbracoContextKey, out var obj) && obj is UmbracoContext umbracoContext) + return umbracoContext; + + // not sure this is a good idea at all + //return Current.UmbracoContext; + + // better fail fast + if (throwIfMissing) + throw new InvalidOperationException("AutoMapper ResolutionContext does not contain an UmbracoContext."); + + return null; + } + } +} diff --git a/src/Umbraco.Web/Models/Mapping/ContentChildOfListViewResolver.cs b/src/Umbraco.Web/Models/Mapping/ContentChildOfListViewResolver.cs new file mode 100644 index 0000000000..a2f250d6f3 --- /dev/null +++ b/src/Umbraco.Web/Models/Mapping/ContentChildOfListViewResolver.cs @@ -0,0 +1,26 @@ +using AutoMapper; +using Umbraco.Core.Models; +using Umbraco.Core.Services; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.Web.Models.Mapping +{ + internal class ContentChildOfListViewResolver : IValueResolver + { + private readonly IContentService _contentService; + private readonly IContentTypeService _contentTypeService; + + public ContentChildOfListViewResolver(IContentService contentService, IContentTypeService contentTypeService) + { + _contentService = contentService; + _contentTypeService = contentTypeService; + } + + public bool Resolve(IContent source, ContentItemDisplay destination, bool destMember, ResolutionContext context) + { + // map the IsChildOfListView (this is actually if it is a descendant of a list view!) + var parent = _contentService.GetParent(source); + return parent != null && (parent.ContentType.IsContainer || _contentTypeService.HasContainerInPath(parent.Path)); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/Mapping/ContentMapperProfile.cs b/src/Umbraco.Web/Models/Mapping/ContentMapperProfile.cs index dff38938e0..16f14c83d8 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentMapperProfile.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentMapperProfile.cs @@ -1,21 +1,28 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Web; -using System.Web.Mvc; +using System.Linq; using AutoMapper; using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Services; -using Umbraco.Web.Composing; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Routing; using Umbraco.Web.Trees; -using Umbraco.Web._Legacy.Actions; namespace Umbraco.Web.Models.Mapping { + internal class ContentUrlResolver : IValueResolver + { + public string[] Resolve(IContent source, ContentItemDisplay destination, string[] destMember, ResolutionContext context) + { + var umbracoContext = context.GetUmbracoContext(); + + var urls = umbracoContext == null + ? new[] {"Cannot generate urls without a current Umbraco Context"} + : source.GetContentUrls(umbracoContext).ToArray(); + + return urls; + } + } + /// /// Declares how model mappings for content /// @@ -26,13 +33,17 @@ namespace Umbraco.Web.Models.Mapping // create, capture, cache var contentOwnerResolver = new OwnerResolver(userService); var creatorResolver = new CreatorResolver(userService); - var actionButtonsResolver = new ActionButtonsResolver(new Lazy(() => userService)); - var tabsAndPropertiesResolver = new TabsAndPropertiesResolver(textService); + var actionButtonsResolver = new ActionButtonsResolver(userService, contentService); + var tabsAndPropertiesResolver = new TabsAndPropertiesResolver(textService); + var childOfListViewResolver = new ContentChildOfListViewResolver(contentService, contentTypeService); + var contentTypeBasicResolver = new ContentTypeBasicResolver(); + var contentTreeNodeUrlResolver = new ContentTreeNodeUrlResolver(); + var defaultTemplateResolver = new DefaultTemplateResolver(); + var contentUrlResolver = new ContentUrlResolver(); //FROM IContent TO ContentItemDisplay CreateMap() - .ForMember(dest => dest.Udi, opt => opt.MapFrom(src => - Udi.Create(src.Blueprint ? Constants.UdiEntityType.DocumentBluePrint : Constants.UdiEntityType.Document, src.Key))) + .ForMember(dest => dest.Udi, opt => opt.MapFrom(src => Udi.Create(src.Blueprint ? 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)) @@ -40,29 +51,35 @@ namespace Umbraco.Web.Models.Mapping .ForMember(dest => dest.ContentTypeName, opt => opt.MapFrom(src => src.ContentType.Name)) .ForMember(dest => dest.IsContainer, opt => opt.MapFrom(src => src.ContentType.IsContainer)) .ForMember(dest => dest.IsBlueprint, opt => opt.MapFrom(src => src.Blueprint)) - .ForMember(dest => dest.IsChildOfListView, opt => opt.Ignore()) + .ForMember(dest => dest.IsChildOfListView, opt => opt.ResolveUsing(childOfListViewResolver)) .ForMember(dest => dest.Trashed, opt => opt.MapFrom(src => src.Trashed)) .ForMember(dest => dest.PublishDate, opt => opt.MapFrom(src => src.PublishDate)) - .ForMember(dest => dest.TemplateAlias, opt => opt.MapFrom(src => src.Template.Alias)) - .ForMember(dest => dest.Urls, opt => opt.MapFrom(src => - UmbracoContext.Current == null - ? new[] {"Cannot generate urls without a current Umbraco Context"} - : src.GetContentUrls(UmbracoContext.Current))) + .ForMember(dest => dest.TemplateAlias, opt => opt.ResolveUsing(defaultTemplateResolver)) + .ForMember(dest => dest.Urls, opt => opt.ResolveUsing(contentUrlResolver)) .ForMember(dest => dest.Properties, opt => opt.Ignore()) .ForMember(dest => dest.AllowPreview, opt => opt.Ignore()) - .ForMember(dest => dest.TreeNodeUrl, opt => opt.Ignore()) + .ForMember(dest => dest.TreeNodeUrl, opt => opt.ResolveUsing(contentTreeNodeUrlResolver)) .ForMember(dest => dest.Notifications, opt => opt.Ignore()) .ForMember(dest => dest.Errors, opt => opt.Ignore()) .ForMember(dest => dest.Alias, opt => opt.Ignore()) - .ForMember(dest => dest.Tabs, opt => opt.ResolveUsing(src => tabsAndPropertiesResolver.Resolve(src))) + .ForMember(dest => dest.DocumentType, opt => opt.ResolveUsing(contentTypeBasicResolver)) + .ForMember(dest => dest.AllowedTemplates, opt => + opt.MapFrom(content => content.ContentType.AllowedTemplates + .Where(t => t.Alias.IsNullOrWhiteSpace() == false && t.Name.IsNullOrWhiteSpace() == false) + .ToDictionary(t => t.Alias, t => t.Name))) + .ForMember(dest => dest.Tabs, opt => opt.ResolveUsing(tabsAndPropertiesResolver)) .ForMember(dest => dest.AllowedActions, opt => opt.ResolveUsing(src => actionButtonsResolver.Resolve(src))) .ForMember(dest => dest.AdditionalData, opt => opt.Ignore()) - .AfterMap((src, dest) => AfterMap(src, dest, dataTypeService, textService, contentTypeService, contentService)); + .AfterMap((content, display) => + { + if (content.ContentType.IsContainer) + TabsAndPropertiesResolver.AddListView(display, "content", dataTypeService, textService); + }); //FROM IContent TO ContentItemBasic CreateMap>() .ForMember(dest => dest.Udi, opt => opt.MapFrom(src => - Udi.Create(src.Blueprint ? Constants.UdiEntityType.DocumentBluePrint : Constants.UdiEntityType.Document, src.Key))) + Udi.Create(src.Blueprint ? 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)) @@ -74,141 +91,12 @@ namespace Umbraco.Web.Models.Mapping //FROM IContent TO ContentItemDto CreateMap>() .ForMember(dest => dest.Udi, opt => opt.MapFrom(src => - Udi.Create(src.Blueprint ? Constants.UdiEntityType.DocumentBluePrint : Constants.UdiEntityType.Document, src.Key))) + Udi.Create(src.Blueprint ? 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.Ignore()) .ForMember(dest => dest.Icon, opt => opt.Ignore()) .ForMember(dest => dest.Alias, opt => opt.Ignore()) .ForMember(dest => dest.AdditionalData, opt => opt.Ignore()); } - - /// - /// Maps the generic tab with custom properties for content - /// - /// - /// - /// - /// - /// - /// - private static void AfterMap(IContent content, ContentItemDisplay display, IDataTypeService dataTypeService, - ILocalizedTextService localizedText, IContentTypeService contentTypeService, IContentService contentService) - { - // map the IsChildOfListView (this is actually if it is a descendant of a list view!) - var parent = content.Parent(contentService); - display.IsChildOfListView = parent != null && (parent.ContentType.IsContainer || contentTypeService.HasContainerInPath(parent.Path)); - - //map the tree node url - if (HttpContext.Current != null) - { - var urlHelper = new UrlHelper(HttpContext.Current.Request.RequestContext); - var url = urlHelper.GetUmbracoApiService(controller => controller.GetTreeNode(display.Id.ToString(), null)); - display.TreeNodeUrl = url; - } - - //fill in the template config to be passed to the template drop down. - var templateItemConfig = new Dictionary {{"", localizedText.Localize("general/choose") } }; - foreach (var t in content.ContentType.AllowedTemplates - .Where(t => t.Alias.IsNullOrWhiteSpace() == false && t.Name.IsNullOrWhiteSpace() == false)) - { - templateItemConfig.Add(t.Alias, t.Name); - } - - if (content.ContentType.IsContainer) - { - TabsAndPropertiesResolver.AddListView(display, "content", dataTypeService, localizedText); - } - - var properties = new List - { - new ContentPropertyDisplay - { - Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}doctype", - Label = localizedText.Localize("content/documentType"), - Value = localizedText.UmbracoDictionaryTranslate(display.ContentTypeName), - View = Current.PropertyEditors[Constants.PropertyEditors.Aliases.NoEdit].GetValueEditor().View - }, - new ContentPropertyDisplay - { - Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}releasedate", - Label = localizedText.Localize("content/releaseDate"), - Value = display.ReleaseDate?.ToIsoString(), - //Not editible for people without publish permission (U4-287) - View = display.AllowedActions.Contains(ActionPublish.Instance.Letter.ToString(CultureInfo.InvariantCulture)) ? "datepicker" : Current.PropertyEditors[Constants.PropertyEditors.Aliases.NoEdit].GetValueEditor().View, - Config = new Dictionary - { - {"offsetTime", "1"} - } - //TODO: Fix up hard coded datepicker - }, - new ContentPropertyDisplay - { - Alias = string.Format("{0}expiredate", Constants.PropertyEditors.InternalGenericPropertiesPrefix), - Label = localizedText.Localize("content/unpublishDate"), - Value = display.ExpireDate.HasValue ? display.ExpireDate.Value.ToIsoString() : null, - //Not editible for people without publish permission (U4-287) - View = display.AllowedActions.Contains(ActionPublish.Instance.Letter.ToString(CultureInfo.InvariantCulture)) ? "datepicker" : Current.PropertyEditors[Constants.PropertyEditors.Aliases.NoEdit].GetValueEditor().View, - Config = new Dictionary - { - {"offsetTime", "1"} - } - //TODO: Fix up hard coded datepicker - }, - new ContentPropertyDisplay - { - Alias = string.Format("{0}template", Constants.PropertyEditors.InternalGenericPropertiesPrefix), - Label = localizedText.Localize("template/template"), - Value = display.TemplateAlias, - View = "dropdown", //TODO: Hard coding until we make a real dropdown property editor to lookup - Config = new Dictionary - { - {"items", templateItemConfig} - } - } - }; - - - TabsAndPropertiesResolver.MapGenericProperties(content, display, localizedText, properties.ToArray(), - genericProperties => - { - //TODO: This would be much nicer with the IUmbracoContextAccessor so we don't use singletons - //If this is a web request and there's a user signed in and the - // user has access to the settings section, we will - if (HttpContext.Current != null && UmbracoContext.Current != null && UmbracoContext.Current.Security.CurrentUser != null - && UmbracoContext.Current.Security.CurrentUser.AllowedSections.Any(x => x.Equals(Constants.Applications.Settings))) - { - var currentDocumentType = contentTypeService.Get(display.ContentTypeAlias); - var currentDocumentTypeName = currentDocumentType == null ? string.Empty : localizedText.UmbracoDictionaryTranslate(currentDocumentType.Name); - - var currentDocumentTypeId = currentDocumentType == null ? string.Empty : currentDocumentType.Id.ToString(CultureInfo.InvariantCulture); - //TODO: Hard coding this is not good - var docTypeLink = string.Format("#/settings/documenttypes/edit/{0}", currentDocumentTypeId); - - //Replace the doc type property - var docTypeProperty = genericProperties.First(x => x.Alias == string.Format("{0}doctype", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); - docTypeProperty.Value = new List - { - new - { - linkText = currentDocumentTypeName, - url = docTypeLink, - target = "_self", - icon = "icon-item-arrangement" - } - }; - //TODO: Hard coding this because the templatepicker doesn't necessarily need to be a resolvable (real) property editor - docTypeProperty.View = "urllist"; - } - - // inject 'Link to document' as the first generic property - genericProperties.Insert(0, new ContentPropertyDisplay - { - Alias = string.Format("{0}urls", Constants.PropertyEditors.InternalGenericPropertiesPrefix), - Label = localizedText.Localize("content/urls"), - Value = string.Join(",", display.Urls), - View = "urllist" //TODO: Hard coding this because the templatepicker doesn't necessarily need to be a resolvable (real) property editor - }); - }); - } } } diff --git a/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicConverter.cs b/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicConverter.cs index b4f0436660..aff472d306 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicConverter.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicConverter.cs @@ -15,13 +15,11 @@ namespace Umbraco.Web.Models.Mapping internal class ContentPropertyBasicConverter : ITypeConverter where TDestination : ContentPropertyBasic, new() { - private readonly Lazy _dataTypeService; + protected IDataTypeService DataTypeService { get; } - protected IDataTypeService DataTypeService => _dataTypeService.Value; - - public ContentPropertyBasicConverter(Lazy dataTypeService) + public ContentPropertyBasicConverter(IDataTypeService dataTypeService) { - _dataTypeService = dataTypeService; + DataTypeService = dataTypeService; } /// diff --git a/src/Umbraco.Web/Models/Mapping/ContentPropertyDisplayConverter.cs b/src/Umbraco.Web/Models/Mapping/ContentPropertyDisplayConverter.cs index 99540fc6ea..c3a5750078 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentPropertyDisplayConverter.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentPropertyDisplayConverter.cs @@ -15,10 +15,13 @@ namespace Umbraco.Web.Models.Mapping /// internal class ContentPropertyDisplayConverter : ContentPropertyBasicConverter { - public ContentPropertyDisplayConverter(Lazy dataTypeService) - : base(dataTypeService) - { } + private readonly ILocalizedTextService _textService; + public ContentPropertyDisplayConverter(IDataTypeService dataTypeService, ILocalizedTextService textService) + : base(dataTypeService) + { + _textService = textService; + } public override ContentPropertyDisplay Convert(Property originalProp, ContentPropertyDisplay dest, ResolutionContext context) { var display = base.Convert(originalProp, dest, context); @@ -58,6 +61,10 @@ namespace Umbraco.Web.Models.Mapping display.View = valEditor.View; } + //Translate + display.Label = _textService.UmbracoDictionaryTranslate(display.Label); + display.Description = _textService.UmbracoDictionaryTranslate(display.Description); + return display; } } diff --git a/src/Umbraco.Web/Models/Mapping/ContentPropertyDtoConverter.cs b/src/Umbraco.Web/Models/Mapping/ContentPropertyDtoConverter.cs index c1cad75674..66f42fc2ad 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentPropertyDtoConverter.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentPropertyDtoConverter.cs @@ -11,7 +11,7 @@ namespace Umbraco.Web.Models.Mapping /// internal class ContentPropertyDtoConverter : ContentPropertyBasicConverter { - public ContentPropertyDtoConverter(Lazy dataTypeService) + public ContentPropertyDtoConverter(IDataTypeService dataTypeService) : base(dataTypeService) { } diff --git a/src/Umbraco.Web/Models/Mapping/ContentPropertyMapperProfile.cs b/src/Umbraco.Web/Models/Mapping/ContentPropertyMapperProfile.cs index 26cb6c40a5..7764613a0e 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentPropertyMapperProfile.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentPropertyMapperProfile.cs @@ -1,5 +1,4 @@ -using System; -using AutoMapper; +using AutoMapper; using Umbraco.Core.Models; using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; @@ -12,13 +11,11 @@ namespace Umbraco.Web.Models.Mapping /// internal class ContentPropertyMapperProfile : Profile { - private readonly IDataTypeService _dataTypeService; - - public ContentPropertyMapperProfile(IDataTypeService dataTypeService) + public ContentPropertyMapperProfile(IDataTypeService dataTypeService, ILocalizedTextService textService) { - _dataTypeService = dataTypeService; - - var lazyDataTypeService = new Lazy(() => _dataTypeService); + var contentPropertyBasicConverter = new ContentPropertyBasicConverter(dataTypeService); + var contentPropertyDtoConverter = new ContentPropertyDtoConverter(dataTypeService); + var contentPropertyDisplayConverter = new ContentPropertyDisplayConverter(dataTypeService, textService); //FROM Property TO ContentPropertyBasic CreateMap>() @@ -28,16 +25,13 @@ namespace Umbraco.Web.Models.Mapping .ForMember(tab => tab.Alias, expression => expression.Ignore()); //FROM Property TO ContentPropertyBasic - CreateMap() - .ConvertUsing(new ContentPropertyBasicConverter(lazyDataTypeService)); + CreateMap().ConvertUsing(contentPropertyBasicConverter); //FROM Property TO ContentPropertyDto - CreateMap() - .ConvertUsing(new ContentPropertyDtoConverter(lazyDataTypeService)); + CreateMap().ConvertUsing(contentPropertyDtoConverter); //FROM Property TO ContentPropertyDisplay - CreateMap() - .ConvertUsing(new ContentPropertyDisplayConverter(lazyDataTypeService)); + CreateMap().ConvertUsing(contentPropertyDisplayConverter); } } } diff --git a/src/Umbraco.Web/Models/Mapping/ContentTreeNodeUrlResolver.cs b/src/Umbraco.Web/Models/Mapping/ContentTreeNodeUrlResolver.cs new file mode 100644 index 0000000000..48400f51f6 --- /dev/null +++ b/src/Umbraco.Web/Models/Mapping/ContentTreeNodeUrlResolver.cs @@ -0,0 +1,24 @@ +using System.Web.Mvc; +using AutoMapper; +using Umbraco.Core.Models; +using Umbraco.Web.Trees; + +namespace Umbraco.Web.Models.Mapping +{ + /// + /// Gets the tree node url for the content or media + /// + internal class ContentTreeNodeUrlResolver : IValueResolver + where TSource : IContentBase + where TController : ContentTreeControllerBase + { + public string Resolve(TSource source, object destination, string destMember, ResolutionContext context) + { + var umbracoContext = context.GetUmbracoContext(throwIfMissing: false); + if (umbracoContext == null) return null; + + var urlHelper = new UrlHelper(umbracoContext.HttpContext.Request.RequestContext); + return urlHelper.GetUmbracoApiService(controller => controller.GetTreeNode(source.Key.ToString("N"), null)); + } + } +} diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeBasicResolver.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeBasicResolver.cs new file mode 100644 index 0000000000..db32908352 --- /dev/null +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeBasicResolver.cs @@ -0,0 +1,39 @@ +using System; +using System.Linq; +using System.Web; +using AutoMapper; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.Web.Models.Mapping +{ + /// + /// Resolves a from the item and checks if the current user + /// has access to see this data + /// + internal class ContentTypeBasicResolver : IValueResolver + where TSource : IContentBase + { + public ContentTypeBasic Resolve(TSource source, TDestination destination, ContentTypeBasic destMember, ResolutionContext context) + { + //TODO: We can resolve the UmbracoContext from the IValueResolver options! + // OMG + if (HttpContext.Current != null && UmbracoContext.Current != null && UmbracoContext.Current.Security.CurrentUser != null + && UmbracoContext.Current.Security.CurrentUser.AllowedSections.Any(x => x.Equals(Constants.Applications.Settings))) + { + ContentTypeBasic contentTypeBasic; + if (source is IContent content) + contentTypeBasic = Mapper.Map(content.ContentType); + else if (source is IMedia media) + contentTypeBasic = Mapper.Map(media.ContentType); + else + throw new NotSupportedException($"Expected TSource to be IContent or IMedia, got {typeof(TSource).Name}."); + + return contentTypeBasic; + } + //no access + return null; + } + } +} diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeMapperProfile.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeMapperProfile.cs index fcfdddfc7b..c9e9554b93 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentTypeMapperProfile.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeMapperProfile.cs @@ -14,39 +14,8 @@ namespace Umbraco.Web.Models.Mapping /// internal class ContentTypeMapperProfile : Profile { - private readonly PropertyEditorCollection _propertyEditors; - private readonly IDataTypeService _dataTypeService; - private readonly IFileService _fileService; - private readonly IContentTypeService _contentTypeService; - private readonly IMediaTypeService _mediaTypeService; - public ContentTypeMapperProfile(PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, IFileService fileService, IContentTypeService contentTypeService, IMediaTypeService mediaTypeService) { - _propertyEditors = propertyEditors; - _dataTypeService = dataTypeService; - _fileService = fileService; - _contentTypeService = contentTypeService; - _mediaTypeService = mediaTypeService; - - // v7 creates this map twice which makes no sense, and AutoMapper 6 detects it - // assuming the second map took over, and removing the first one for now - /* - CreateMap() - .ConstructUsing(basic => new PropertyType(_dataTypeService.GetDataTypeDefinitionById(basic.DataTypeId))) - .ForMember(type => type.ValidationRegExp, opt => opt.ResolveUsing(basic => basic.Validation.Pattern)) - .ForMember(type => type.Mandatory, opt => opt.ResolveUsing(basic => basic.Validation.Mandatory)) - .ForMember(type => type.Name, opt => opt.ResolveUsing(basic => basic.Label)) - .ForMember(type => type.DataTypeDefinitionId, opt => opt.ResolveUsing(basic => basic.DataTypeId)) - .ForMember(type => type.DataTypeId, opt => opt.Ignore()) - .ForMember(type => type.PropertyEditorAlias, opt => opt.Ignore()) - .ForMember(type => type.HelpText, opt => opt.Ignore()) - .ForMember(type => type.Key, opt => opt.Ignore()) - .ForMember(type => type.CreateDate, opt => opt.Ignore()) - .ForMember(type => type.UpdateDate, opt => opt.Ignore()) - .ForMember(type => type.DeletedDate, opt => opt.Ignore()) - .ForMember(type => type.HasIdentity, opt => opt.Ignore()); - */ - CreateMap() //do the base mapping .MapBaseContentTypeSaveToEntity() @@ -57,13 +26,15 @@ namespace Umbraco.Web.Models.Mapping { dest.AllowedTemplates = source.AllowedTemplates .Where(x => x != null) - .Select(s => _fileService.GetTemplate(s)) + .Select(s => fileService.GetTemplate(s)) .ToArray(); if (source.DefaultTemplate != null) - dest.SetDefaultTemplate(_fileService.GetTemplate(source.DefaultTemplate)); + dest.SetDefaultTemplate(fileService.GetTemplate(source.DefaultTemplate)); + else + dest.SetDefaultTemplate(null); - ContentTypeProfileExtensions.AfterMapContentTypeSaveToEntity(source, dest, _contentTypeService); + ContentTypeProfileExtensions.AfterMapContentTypeSaveToEntity(source, dest, contentTypeService); }); CreateMap() @@ -72,7 +43,7 @@ namespace Umbraco.Web.Models.Mapping .ConstructUsing((source) => new MediaType(source.ParentId)) .AfterMap((source, dest) => { - ContentTypeProfileExtensions.AfterMapMediaTypeSaveToEntity(source, dest, _mediaTypeService); + ContentTypeProfileExtensions.AfterMapMediaTypeSaveToEntity(source, dest, mediaTypeService); }); CreateMap() @@ -81,9 +52,9 @@ namespace Umbraco.Web.Models.Mapping .ConstructUsing(source => new MemberType(source.ParentId)) .AfterMap((source, dest) => { - ContentTypeProfileExtensions.AfterMapContentTypeSaveToEntity(source, dest, _contentTypeService); + ContentTypeProfileExtensions.AfterMapContentTypeSaveToEntity(source, dest, contentTypeService); - //map the MemberCanEditProperty,MemberCanViewProperty + //map the MemberCanEditProperty,MemberCanViewProperty,IsSensitiveData foreach (var propertyType in source.Groups.SelectMany(x => x.Properties)) { var localCopy = propertyType; @@ -92,6 +63,7 @@ namespace Umbraco.Web.Models.Mapping { dest.SetMemberCanEditProperty(localCopy.Alias, localCopy.MemberCanEditProperty); dest.SetMemberCanViewProperty(localCopy.Alias, localCopy.MemberCanViewProperty); + dest.SetIsSensitiveProperty(localCopy.Alias, localCopy.IsSensitiveData); } } }); @@ -100,10 +72,10 @@ namespace Umbraco.Web.Models.Mapping CreateMap() //map base logic - .MapBaseContentTypeEntityToDisplay(_propertyEditors, _dataTypeService, _contentTypeService) + .MapBaseContentTypeEntityToDisplay(propertyEditors, dataTypeService, contentTypeService) .AfterMap((memberType, display) => { - //map the MemberCanEditProperty,MemberCanViewProperty + //map the MemberCanEditProperty,MemberCanViewProperty,IsSensitiveData foreach (var propertyType in memberType.PropertyTypes) { var localCopy = propertyType; @@ -112,13 +84,14 @@ namespace Umbraco.Web.Models.Mapping { displayProp.MemberCanEditProperty = memberType.MemberCanEditProperty(localCopy.Alias); displayProp.MemberCanViewProperty = memberType.MemberCanViewProperty(localCopy.Alias); + displayProp.IsSensitiveData = memberType.IsSensitiveProperty(localCopy.Alias); } } }); CreateMap() //map base logic - .MapBaseContentTypeEntityToDisplay(_propertyEditors, _dataTypeService, _contentTypeService) + .MapBaseContentTypeEntityToDisplay(propertyEditors, dataTypeService, contentTypeService) .AfterMap((source, dest) => { //default listview @@ -127,14 +100,14 @@ namespace Umbraco.Web.Models.Mapping if (string.IsNullOrEmpty(source.Name) == false) { var name = Constants.Conventions.DataTypes.ListViewPrefix + source.Name; - if (_dataTypeService.GetDataType(name) != null) + if (dataTypeService.GetDataType(name) != null) dest.ListViewEditorName = name; } }); CreateMap() //map base logic - .MapBaseContentTypeEntityToDisplay(_propertyEditors, _dataTypeService, _contentTypeService) + .MapBaseContentTypeEntityToDisplay(propertyEditors, dataTypeService, contentTypeService) .ForMember(dto => dto.AllowedTemplates, opt => opt.Ignore()) .ForMember(dto => dto.DefaultTemplate, opt => opt.Ignore()) .ForMember(display => display.Notifications, opt => opt.Ignore()) @@ -152,7 +125,7 @@ namespace Umbraco.Web.Models.Mapping if (string.IsNullOrEmpty(source.Alias) == false) { var name = Constants.Conventions.DataTypes.ListViewPrefix + source.Alias; - if (_dataTypeService.GetDataType(name) != null) + if (dataTypeService.GetDataType(name) != null) dest.ListViewEditorName = name; } @@ -175,7 +148,7 @@ namespace Umbraco.Web.Models.Mapping .ConstructUsing(propertyTypeBasic => { - var dataType = _dataTypeService.GetDataType(propertyTypeBasic.DataTypeId); + var dataType = dataTypeService.GetDataType(propertyTypeBasic.DataTypeId); if (dataType == null) throw new NullReferenceException("No data type found with id " + propertyTypeBasic.DataTypeId); return new PropertyType(dataType, propertyTypeBasic.Alias); }) @@ -226,7 +199,7 @@ namespace Umbraco.Web.Models.Mapping //if the dest is set and it's the same as the source, then don't change if (destAllowedTemplateAliases.SequenceEqual(source.AllowedTemplates) == false) { - var templates = _fileService.GetTemplates(source.AllowedTemplates.ToArray()); + var templates = fileService.GetTemplates(source.AllowedTemplates.ToArray()); dest.AllowedTemplates = source.AllowedTemplates .Select(x => Mapper.Map(templates.SingleOrDefault(t => t.Alias == x))) .WhereNotNull() @@ -238,7 +211,7 @@ namespace Umbraco.Web.Models.Mapping //if the dest is set and it's the same as the source, then don't change if (dest.DefaultTemplate == null || source.DefaultTemplate != dest.DefaultTemplate.Alias) { - var template = _fileService.GetTemplate(source.DefaultTemplate); + var template = fileService.GetTemplate(source.DefaultTemplate); dest.DefaultTemplate = template == null ? null : Mapper.Map(template); } } diff --git a/src/Umbraco.Web/Models/Mapping/DefaultTemplateResolver.cs b/src/Umbraco.Web/Models/Mapping/DefaultTemplateResolver.cs new file mode 100644 index 0000000000..ae32e7a691 --- /dev/null +++ b/src/Umbraco.Web/Models/Mapping/DefaultTemplateResolver.cs @@ -0,0 +1,24 @@ +using AutoMapper; +using Umbraco.Core.Models; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.Web.Models.Mapping +{ + internal class DefaultTemplateResolver : IValueResolver + { + public string Resolve(IContent source, ContentItemDisplay destination, string destMember, ResolutionContext context) + { + if (source == null || source.Template == null) return null; + + var alias = source.Template.Alias; + + //set default template if template isn't set + if (string.IsNullOrEmpty(alias)) + alias = source.ContentType.DefaultTemplate == null + ? string.Empty + : source.ContentType.DefaultTemplate.Alias; + + return alias; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/Mapping/EntityMapperProfile.cs b/src/Umbraco.Web/Models/Mapping/EntityMapperProfile.cs index 27116e8cdc..38440c0b74 100644 --- a/src/Umbraco.Web/Models/Mapping/EntityMapperProfile.cs +++ b/src/Umbraco.Web/Models/Mapping/EntityMapperProfile.cs @@ -27,7 +27,7 @@ namespace Umbraco.Web.Models.Mapping CreateMap() .ForMember(dest => dest.Udi, opt => opt.MapFrom(src => Udi.Create(ObjectTypes.GetUdiType(src.NodeObjectType), src.Key))) .ForMember(dest => dest.Icon, opt => opt.MapFrom(src => GetContentTypeIcon(src))) - .ForMember(dest => dest.Trashed, opt => opt.Ignore()) + .ForMember(dest => dest.Trashed, opt => opt.MapFrom(src => src.Trashed)) .ForMember(dest => dest.Alias, opt => opt.Ignore()) .AfterMap((src, dest) => { diff --git a/src/Umbraco.Web/Models/Mapping/MediaChildOfListViewResolver.cs b/src/Umbraco.Web/Models/Mapping/MediaChildOfListViewResolver.cs new file mode 100644 index 0000000000..997c900f84 --- /dev/null +++ b/src/Umbraco.Web/Models/Mapping/MediaChildOfListViewResolver.cs @@ -0,0 +1,26 @@ +using AutoMapper; +using Umbraco.Core.Models; +using Umbraco.Core.Services; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.Web.Models.Mapping +{ + internal class MediaChildOfListViewResolver : IValueResolver + { + private readonly IMediaService _mediaService; + private readonly IMediaTypeService _mediaTypeService; + + public MediaChildOfListViewResolver(IMediaService mediaService, IMediaTypeService mediaTypeService) + { + _mediaService = mediaService; + _mediaTypeService = mediaTypeService; + } + + public bool Resolve(IMedia source, MediaItemDisplay destination, bool destMember, ResolutionContext context) + { + // map the IsChildOfListView (this is actually if it is a descendant of a list view!) + var parent = _mediaService.GetParent(source); + return parent != null && (parent.ContentType.IsContainer || _mediaTypeService.HasContainerInPath(parent.Path)); + } + } +} diff --git a/src/Umbraco.Web/Models/Mapping/MediaMapperProfile.cs b/src/Umbraco.Web/Models/Mapping/MediaMapperProfile.cs index 6d3ae36759..540f1d509d 100644 --- a/src/Umbraco.Web/Models/Mapping/MediaMapperProfile.cs +++ b/src/Umbraco.Web/Models/Mapping/MediaMapperProfile.cs @@ -1,14 +1,9 @@ -using System.Collections.Generic; -using System.Linq; -using System.Web; -using System.Web.Mvc; -using AutoMapper; +using AutoMapper; using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Services; -using Umbraco.Web.Composing; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Trees; @@ -19,11 +14,14 @@ namespace Umbraco.Web.Models.Mapping /// internal class MediaMapperProfile : Profile { - public MediaMapperProfile(IUserService userService, ILocalizedTextService textService, IDataTypeService dataTypeService, IMediaService mediaService, ILogger logger) + public MediaMapperProfile(IUserService userService, ILocalizedTextService textService, IDataTypeService dataTypeService, IMediaService mediaService, IMediaTypeService mediaTypeService, ILogger logger) { // create, capture, cache var mediaOwnerResolver = new OwnerResolver(userService); - var tabsAndPropertiesResolver = new TabsAndPropertiesResolver(textService); + var tabsAndPropertiesResolver = new TabsAndPropertiesResolver(textService); + var childOfListViewResolver = new MediaChildOfListViewResolver(mediaService, mediaTypeService); + var contentTreeNodeUrlResolver = new ContentTreeNodeUrlResolver(); + var mediaTypeBasicResolver = new ContentTypeBasicResolver(); //FROM IMedia TO MediaItemDisplay CreateMap() @@ -31,20 +29,26 @@ namespace Umbraco.Web.Models.Mapping .ForMember(dest => dest.Owner, opt => opt.ResolveUsing(src => mediaOwnerResolver.Resolve(src))) .ForMember(dest => dest.Icon, opt => opt.MapFrom(content => content.ContentType.Icon)) .ForMember(dest => dest.ContentTypeAlias, opt => opt.MapFrom(content => content.ContentType.Alias)) - .ForMember(dest => dest.IsChildOfListView, opt => opt.Ignore()) + .ForMember(dest => dest.IsChildOfListView, opt => opt.ResolveUsing(childOfListViewResolver)) .ForMember(dest => dest.Trashed, opt => opt.MapFrom(content => content.Trashed)) .ForMember(dest => dest.ContentTypeName, opt => opt.MapFrom(content => content.ContentType.Name)) .ForMember(dest => dest.Properties, opt => opt.Ignore()) - .ForMember(dest => dest.TreeNodeUrl, opt => opt.Ignore()) + .ForMember(dest => dest.TreeNodeUrl, opt => opt.ResolveUsing(contentTreeNodeUrlResolver)) .ForMember(dest => dest.Notifications, opt => opt.Ignore()) .ForMember(dest => dest.Errors, opt => opt.Ignore()) .ForMember(dest => dest.Published, opt => opt.Ignore()) .ForMember(dest => dest.Updater, opt => opt.Ignore()) .ForMember(dest => dest.Alias, opt => opt.Ignore()) .ForMember(dest => dest.IsContainer, opt => opt.Ignore()) - .ForMember(dest => dest.Tabs, opt => opt.ResolveUsing(src => tabsAndPropertiesResolver.Resolve(src))) + .ForMember(dest => dest.Tabs, opt => opt.ResolveUsing(tabsAndPropertiesResolver)) .ForMember(dest => dest.AdditionalData, opt => opt.Ignore()) - .AfterMap((src, dest) => AfterMap(src, dest, dataTypeService, textService, logger, mediaService)); + .ForMember(dest => dest.ContentType, opt => opt.ResolveUsing(mediaTypeBasicResolver)) + .ForMember(dest => dest.MediaLink, opt => opt.ResolveUsing(content => string.Join(",", content.GetUrls(UmbracoConfig.For.UmbracoSettings().Content, logger)))) + .AfterMap((media, display) => + { + if (media.ContentType.IsContainer) + TabsAndPropertiesResolver.AddListView(display, "media", dataTypeService, textService); + }); //FROM IMedia TO ContentItemBasic CreateMap>() @@ -68,74 +72,5 @@ namespace Umbraco.Web.Models.Mapping .ForMember(dest => dest.Alias, opt => opt.Ignore()) .ForMember(dest => dest.AdditionalData, opt => opt.Ignore()); } - - private static void AfterMap(IMedia media, MediaItemDisplay display, IDataTypeService dataTypeService, ILocalizedTextService localizedText, ILogger logger, IMediaService mediaService) - { - // Adapted from ContentModelMapper - //map the IsChildOfListView (this is actually if it is a descendant of a list view!) - var parent = media.Parent(); - display.IsChildOfListView = parent != null && (parent.ContentType.IsContainer || Current.Services.ContentTypeService.HasContainerInPath(parent.Path)); - - //map the tree node url - if (HttpContext.Current != null) - { - var urlHelper = new UrlHelper(HttpContext.Current.Request.RequestContext); - var url = urlHelper.GetUmbracoApiService(controller => controller.GetTreeNode(display.Id.ToString(), null)); - display.TreeNodeUrl = url; - } - - if (media.ContentType.IsContainer) - { - TabsAndPropertiesResolver.AddListView(display, "media", dataTypeService, localizedText); - } - - var genericProperties = new List - { - new ContentPropertyDisplay - { - Alias = string.Format("{0}doctype", Constants.PropertyEditors.InternalGenericPropertiesPrefix), - Label = localizedText.Localize("content/mediatype"), - Value = localizedText.UmbracoDictionaryTranslate(display.ContentTypeName), - View = Current.PropertyEditors[Constants.PropertyEditors.Aliases.NoEdit].GetValueEditor().View - } - }; - - TabsAndPropertiesResolver.MapGenericProperties(media, display, localizedText, genericProperties, properties => - { - if (HttpContext.Current != null && UmbracoContext.Current != null && UmbracoContext.Current.Security.CurrentUser != null - && UmbracoContext.Current.Security.CurrentUser.AllowedSections.Any(x => x.Equals(Constants.Applications.Settings))) - { - var mediaTypeLink = string.Format("#/settings/mediatypes/edit/{0}", media.ContentTypeId); - - //Replace the doctype property - var docTypeProperty = properties.First(x => x.Alias == string.Format("{0}doctype", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); - docTypeProperty.Value = new List - { - new - { - linkText = media.ContentType.Name, - url = mediaTypeLink, - target = "_self", - icon = "icon-item-arrangement" - } - }; - docTypeProperty.View = "urllist"; - } - - // inject 'Link to media' as the first generic property - var links = media.GetUrls(UmbracoConfig.For.UmbracoSettings().Content, logger); - if (links.Any()) - { - var link = new ContentPropertyDisplay - { - Alias = string.Format("{0}urls", Constants.PropertyEditors.InternalGenericPropertiesPrefix), - Label = localizedText.Localize("media/urls"), - Value = string.Join(",", links), - View = "urllist" - }; - properties.Insert(0, link); - } - }); - } } } diff --git a/src/Umbraco.Web/Models/Mapping/MemberBasicPropertiesResolver.cs b/src/Umbraco.Web/Models/Mapping/MemberBasicPropertiesResolver.cs new file mode 100644 index 0000000000..09d0657530 --- /dev/null +++ b/src/Umbraco.Web/Models/Mapping/MemberBasicPropertiesResolver.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.Linq; +using AutoMapper; +using Umbraco.Core.Models; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.Web.Models.Mapping +{ + /// + /// A resolver to map properties to a collection of + /// + internal class MemberBasicPropertiesResolver : IValueResolver> + { + public IEnumerable Resolve(IMember source, MemberBasic destination, IEnumerable destMember, ResolutionContext context) + { + var umbracoContext = context.GetUmbracoContext(); + + var result = Mapper.Map, IEnumerable>( + // Sort properties so items from different compositions appear in correct order (see U4-9298). Map sorted properties. + source.Properties.OrderBy(prop => prop.PropertyType.SortOrder)) + .ToList(); + + var memberType = source.ContentType; + + //now update the IsSensitive value + foreach (var prop in result) + { + //check if this property is flagged as sensitive + var isSensitiveProperty = memberType.IsSensitiveProperty(prop.Alias); + //check permissions for viewing sensitive data + if (isSensitiveProperty && umbracoContext.Security.CurrentUser.HasAccessToSensitiveData() == false) + { + //mark this property as sensitive + prop.IsSensitive = true; + //clear the value + prop.Value = null; + } + } + return result; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/Mapping/MemberMapperProfile.cs b/src/Umbraco.Web/Models/Mapping/MemberMapperProfile.cs index 24f5b5ebfd..eac93657f4 100644 --- a/src/Umbraco.Web/Models/Mapping/MemberMapperProfile.cs +++ b/src/Umbraco.Web/Models/Mapping/MemberMapperProfile.cs @@ -1,19 +1,11 @@ using System; -using System.Collections.Generic; -using System.Web; -using System.Web.Mvc; using System.Web.Security; using AutoMapper; using Umbraco.Core; using Umbraco.Core.Models; -using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; -using System.Linq; -using Umbraco.Core.Security; using Umbraco.Core.Services.Implement; -using Umbraco.Web.Composing; -using Umbraco.Web.Trees; namespace Umbraco.Web.Models.Mapping { @@ -26,18 +18,15 @@ namespace Umbraco.Web.Models.Mapping { // create, capture, cache var memberOwnerResolver = new OwnerResolver(userService); - var tabsAndPropertiesResolver = new MemberTabsAndPropertiesResolver(textService); + var tabsAndPropertiesResolver = new MemberTabsAndPropertiesResolver(textService, memberService, userService); var memberProfiderFieldMappingResolver = new MemberProviderFieldResolver(); - var membershipScenarioMappingResolver = new MembershipScenarioResolver(new Lazy(() => memberTypeService)); + var membershipScenarioMappingResolver = new MembershipScenarioResolver(memberTypeService); var memberDtoPropertiesResolver = new MemberDtoPropertiesResolver(); + var memberTreeNodeUrlResolver = new MemberTreeNodeUrlResolver(); + var memberBasicPropertiesResolver = new MemberBasicPropertiesResolver(); //FROM MembershipUser TO MediaItemDisplay - used when using a non-umbraco membership provider - CreateMap() - .ConvertUsing(user => - { - var member = Mapper.Map(user); - return Mapper.Map(member); - }); + CreateMap().ConvertUsing(); //FROM MembershipUser TO IMember - used when using a non-umbraco membership provider CreateMap() @@ -75,7 +64,7 @@ namespace Umbraco.Web.Models.Mapping .ForMember(dest => dest.ContentTypeAlias, opt => opt.MapFrom(src => src.ContentType.Alias)) .ForMember(dest => dest.ContentTypeName, opt => opt.MapFrom(src => src.ContentType.Name)) .ForMember(dest => dest.Properties, opt => opt.Ignore()) - .ForMember(dest => dest.Tabs, opt => opt.ResolveUsing(src => tabsAndPropertiesResolver.Resolve(src))) + .ForMember(dest => dest.Tabs, opt => opt.ResolveUsing(tabsAndPropertiesResolver)) .ForMember(dest => dest.MemberProviderFieldMapping, opt => opt.ResolveUsing(src => memberProfiderFieldMappingResolver.Resolve(src))) .ForMember(dest => dest.MembershipScenario, opt => opt.ResolveUsing(src => membershipScenarioMappingResolver.Resolve(src))) .ForMember(dest => dest.Notifications, opt => opt.Ignore()) @@ -86,8 +75,7 @@ namespace Umbraco.Web.Models.Mapping .ForMember(dest => dest.IsChildOfListView, opt => opt.Ignore()) .ForMember(dest => dest.Trashed, opt => opt.Ignore()) .ForMember(dest => dest.IsContainer, opt => opt.Ignore()) - .ForMember(dest => dest.TreeNodeUrl, opt => opt.Ignore()) - .AfterMap((src, dest) => MapGenericCustomProperties(memberService, userService, src, dest, textService)); + .ForMember(dest => dest.TreeNodeUrl, opt => opt.ResolveUsing(memberTreeNodeUrlResolver)); //FROM IMember TO MemberBasic CreateMap() @@ -100,7 +88,8 @@ namespace Umbraco.Web.Models.Mapping .ForMember(dest => dest.Trashed, opt => opt.Ignore()) .ForMember(dest => dest.Published, opt => opt.Ignore()) .ForMember(dest => dest.Updater, opt => opt.Ignore()) - .ForMember(dest => dest.Alias, opt => opt.Ignore()); + .ForMember(dest => dest.Alias, opt => opt.Ignore()) + .ForMember(dto => dto.Properties, expression => expression.ResolveUsing(memberBasicPropertiesResolver)); //FROM MembershipUser TO MemberBasic CreateMap() @@ -110,7 +99,7 @@ namespace Umbraco.Web.Models.Mapping .ForMember(dest => dest.CreateDate, opt => opt.MapFrom(src => src.CreationDate)) .ForMember(dest => dest.UpdateDate, opt => opt.MapFrom(src => src.LastActivityDate)) .ForMember(dest => dest.Key, opt => opt.MapFrom(src => src.ProviderUserKey.TryConvertTo().Result.ToString("N"))) - .ForMember(dest => dest.Owner, opt => opt.UseValue(new ContentEditing.UserProfile {Name = "Admin", UserId = 0})) + .ForMember(dest => dest.Owner, opt => opt.UseValue(new UserProfile {Name = "Admin", UserId = 0})) .ForMember(dest => dest.Icon, opt => opt.UseValue("icon-user")) .ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.UserName)) .ForMember(dest => dest.Email, opt => opt.MapFrom(src => src.Email)) @@ -148,170 +137,5 @@ namespace Umbraco.Web.Models.Mapping .ForMember(dest => dest.Alias, opt => opt.Ignore()) .ForMember(dest => dest.Path, opt => opt.Ignore()); } - - /// - /// Maps the generic tab with custom properties for content - /// - /// - /// - /// - /// - /// - /// - /// If this is a new entity and there is an approved field then we'll set it to true by default. - /// - private static void MapGenericCustomProperties(IMemberService memberService, IUserService userService, IMember member, MemberDisplay display, ILocalizedTextService localizedText) - { - var membersProvider = Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider(); - - //map the tree node url - if (HttpContext.Current != null) - { - var urlHelper = new UrlHelper(HttpContext.Current.Request.RequestContext); - var url = urlHelper.GetUmbracoApiService(controller => controller.GetTreeNode(display.Key.ToString("N"), null)); - display.TreeNodeUrl = url; - } - - var genericProperties = new List - { - new ContentPropertyDisplay - { - Alias = string.Format("{0}doctype", Constants.PropertyEditors.InternalGenericPropertiesPrefix), - Label = localizedText.Localize("content/membertype"), - Value = localizedText.UmbracoDictionaryTranslate(display.ContentTypeName), - View = Current.PropertyEditors[Constants.PropertyEditors.Aliases.NoEdit].GetValueEditor().View - }, - GetLoginProperty(memberService, member, display, localizedText), - new ContentPropertyDisplay - { - Alias = string.Format("{0}email", Constants.PropertyEditors.InternalGenericPropertiesPrefix), - Label = localizedText.Localize("general/email"), - Value = display.Email, - View = "email", - Validation = {Mandatory = true} - }, - new ContentPropertyDisplay - { - Alias = string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix), - Label = localizedText.Localize("password"), - //NOTE: The value here is a json value - but the only property we care about is the generatedPassword one if it exists, the newPassword exists - // only when creating a new member and we want to have a generated password pre-filled. - Value = new Dictionary - { - // fixme why ignoreCase, what are we doing here?! - {"generatedPassword", member.GetAdditionalDataValueIgnoreCase("GeneratedPassword", null)}, - {"newPassword", member.GetAdditionalDataValueIgnoreCase("NewPassword", null)}, - }, - //TODO: Hard coding this because the changepassword doesn't necessarily need to be a resolvable (real) property editor - View = "changepassword", - //initialize the dictionary with the configuration from the default membership provider - Config = new Dictionary(membersProvider.GetConfiguration(userService)) - { - //the password change toggle will only be displayed if there is already a password assigned. - {"hasPassword", member.RawPasswordValue.IsNullOrWhiteSpace() == false} - } - }, - new ContentPropertyDisplay - { - Alias = string.Format("{0}membergroup", Constants.PropertyEditors.InternalGenericPropertiesPrefix), - Label = localizedText.Localize("content/membergroup"), - Value = GetMemberGroupValue(display.Username), - View = "membergroups", - Config = new Dictionary {{"IsRequired", true}} - } - }; - - TabsAndPropertiesResolver.MapGenericProperties(member, display, localizedText, genericProperties, properties => - { - if (HttpContext.Current != null && UmbracoContext.Current != null && UmbracoContext.Current.Security.CurrentUser != null - && UmbracoContext.Current.Security.CurrentUser.AllowedSections.Any(x => x.Equals(Constants.Applications.Settings))) - { - var memberTypeLink = string.Format("#/member/memberTypes/edit/{0}", member.ContentTypeId); - - //Replace the doctype property - var docTypeProperty = properties.First(x => x.Alias == string.Format("{0}doctype", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); - docTypeProperty.Value = new List - { - new - { - linkText = member.ContentType.Name, - url = memberTypeLink, - target = "_self", - icon = "icon-item-arrangement" - } - }; - docTypeProperty.View = "urllist"; - } - }); - - //check if there's an approval field - var provider = membersProvider as IUmbracoMemberTypeMembershipProvider; - if (member.HasIdentity == false && provider != null) - { - var approvedField = provider.ApprovedPropertyTypeAlias; - var prop = display.Properties.FirstOrDefault(x => x.Alias == approvedField); - if (prop != null) - { - prop.Value = 1; - } - } - } - - /// - /// Returns the login property display field - /// - /// - /// - /// - /// - /// - /// - /// If the membership provider installed is the umbraco membership provider, then we will allow changing the username, however if - /// the membership provider is a custom one, we cannot allow chaning the username because MembershipProvider's do not actually natively - /// allow that. - /// - internal static ContentPropertyDisplay GetLoginProperty(IMemberService memberService, IMember member, MemberDisplay display, ILocalizedTextService localizedText) - { - var prop = new ContentPropertyDisplay - { - Alias = string.Format("{0}login", Constants.PropertyEditors.InternalGenericPropertiesPrefix), - Label = localizedText.Localize("login"), - Value = display.Username - }; - - var scenario = memberService.GetMembershipScenario(); - - //only allow editing if this is a new member, or if the membership provider is the umbraco one - if (member.HasIdentity == false || scenario == MembershipScenario.NativeUmbraco) - { - prop.View = "textbox"; - prop.Validation.Mandatory = true; - } - else - { - prop.View = "readonlyvalue"; - } - return prop; - } - - internal static IDictionary GetMemberGroupValue(string username) - { - var userRoles = username.IsNullOrWhiteSpace() ? null : Roles.GetRolesForUser(username); - - // create a dictionary of all roles (except internal roles) + "false" - var result = Roles.GetAllRoles().Distinct() - // if a role starts with __umbracoRole we won't show it as it's an internal role used for public access - .Where(x => x.StartsWith(Constants.Conventions.Member.InternalRolePrefix) == false) - .ToDictionary(x => x, x => false); - - // if user has no roles, just return the dictionary - if (userRoles == null) return result; - - // else update the dictionary to "true" for the user roles (except internal roles) - foreach (var userRole in userRoles.Where(x => x.StartsWith(Constants.Conventions.Member.InternalRolePrefix) == false)) - result[userRole] = true; - - return result; - } } } diff --git a/src/Umbraco.Web/Models/Mapping/MemberTabsAndPropertiesResolver.cs b/src/Umbraco.Web/Models/Mapping/MemberTabsAndPropertiesResolver.cs index b4bfead5e7..5066c4420e 100644 --- a/src/Umbraco.Web/Models/Mapping/MemberTabsAndPropertiesResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/MemberTabsAndPropertiesResolver.cs @@ -1,7 +1,11 @@ using System.Collections.Generic; using System.Linq; +using System.Web.Security; +using AutoMapper; using Umbraco.Core; +using Umbraco.Core.Composing; using Umbraco.Core.Models; +using Umbraco.Core.Models.Membership; using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; @@ -16,44 +20,50 @@ namespace Umbraco.Web.Models.Mapping /// This also ensures that the IsLocked out property is readonly when the member is not locked out - this is because /// an admin cannot actually set isLockedOut = true, they can only unlock. /// - internal class MemberTabsAndPropertiesResolver : TabsAndPropertiesResolver + internal class MemberTabsAndPropertiesResolver : TabsAndPropertiesResolver { private readonly ILocalizedTextService _localizedTextService; + private readonly IMemberService _memberService; + private readonly IUserService _userService; - public MemberTabsAndPropertiesResolver(ILocalizedTextService localizedTextService) + public MemberTabsAndPropertiesResolver(ILocalizedTextService localizedTextService, IMemberService memberService, IUserService userService) : base(localizedTextService) { _localizedTextService = localizedTextService; + _memberService = memberService; + _userService = userService; } - public MemberTabsAndPropertiesResolver(ILocalizedTextService localizedTextService, - IEnumerable ignoreProperties) : base(localizedTextService, ignoreProperties) + public MemberTabsAndPropertiesResolver(ILocalizedTextService localizedTextService, IEnumerable ignoreProperties, IMemberService memberService, IUserService userService) + : base(localizedTextService, ignoreProperties) { _localizedTextService = localizedTextService; + _memberService = memberService; + _userService = userService; } - public override IEnumerable> Resolve(IContentBase content) + /// + /// Overriden to deal with custom member properties and permissions. + public override IEnumerable> Resolve(IMember source, MemberDisplay destination, IEnumerable> destMember, ResolutionContext context) { var provider = Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider(); - IgnoreProperties = content.PropertyTypes + IgnoreProperties = source.PropertyTypes .Where(x => x.HasIdentity == false) .Select(x => x.Alias) .ToArray(); - var result = base.Resolve(content).ToArray(); + var resolved = base.Resolve(source, destination, destMember, context); if (provider.IsUmbracoMembershipProvider() == false) { //it's a generic provider so update the locked out property based on our known constant alias - var isLockedOutProperty = result.SelectMany(x => x.Properties).FirstOrDefault(x => x.Alias == Constants.Conventions.Member.IsLockedOut); - if (isLockedOutProperty != null && isLockedOutProperty.Value.ToString() != "1") + var isLockedOutProperty = resolved.SelectMany(x => x.Properties).FirstOrDefault(x => x.Alias == Constants.Conventions.Member.IsLockedOut); + if (isLockedOutProperty?.Value != null && isLockedOutProperty.Value.ToString() != "1") { isLockedOutProperty.View = "readonlyvalue"; isLockedOutProperty.Value = _localizedTextService.Localize("general/no"); } - - return result; } else { @@ -62,15 +72,186 @@ namespace Umbraco.Web.Models.Mapping //This is kind of a hack because a developer is supposed to be allowed to set their property editor - would have been much easier // if we just had all of the membeship provider fields on the member table :( // TODO: But is there a way to map the IMember.IsLockedOut to the property ? i dunno. - var isLockedOutProperty = result.SelectMany(x => x.Properties).FirstOrDefault(x => x.Alias == umbracoProvider.LockPropertyTypeAlias); - if (isLockedOutProperty != null && isLockedOutProperty.Value.ToString() != "1") + var isLockedOutProperty = resolved.SelectMany(x => x.Properties).FirstOrDefault(x => x.Alias == umbracoProvider.LockPropertyTypeAlias); + if (isLockedOutProperty?.Value != null && isLockedOutProperty.Value.ToString() != "1") { isLockedOutProperty.View = "readonlyvalue"; isLockedOutProperty.Value = _localizedTextService.Localize("general/no"); } - - return result; } + + var umbracoContext = context.GetUmbracoContext(); + if (umbracoContext != null + && umbracoContext.Security.CurrentUser != null + && umbracoContext.Security.CurrentUser.AllowedSections.Any(x => x.Equals(Constants.Applications.Settings))) + { + var memberTypeLink = string.Format("#/member/memberTypes/edit/{0}", source.ContentTypeId); + + //Replace the doctype property + var docTypeProperty = resolved.SelectMany(x => x.Properties) + .First(x => x.Alias == string.Format("{0}doctype", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); + docTypeProperty.Value = new List + { + new + { + linkText = source.ContentType.Name, + url = memberTypeLink, + target = "_self", + icon = "icon-item-arrangement" + } + }; + docTypeProperty.View = "urllist"; + } + + return resolved; + } + + protected override IEnumerable GetCustomGenericProperties(IContentBase content) + { + var member = (IMember) content; + var membersProvider = Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider(); + + var genericProperties = new List + { + new ContentPropertyDisplay + { + Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}doctype", + Label = _localizedTextService.Localize("content/membertype"), + Value = _localizedTextService.UmbracoDictionaryTranslate(member.ContentType.Name), + View = Current.PropertyEditors[Constants.PropertyEditors.Aliases.NoEdit].GetValueEditor().View + }, + GetLoginProperty(_memberService, member, _localizedTextService), + new ContentPropertyDisplay + { + Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}email", + Label = _localizedTextService.Localize("general/email"), + Value = member.Email, + View = "email", + Validation = {Mandatory = true} + }, + new ContentPropertyDisplay + { + Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}password", + Label = _localizedTextService.Localize("password"), + //NOTE: The value here is a json value - but the only property we care about is the generatedPassword one if it exists, the newPassword exists + // only when creating a new member and we want to have a generated password pre-filled. + Value = new Dictionary + { + // fixme why ignoreCase, what are we doing here?! + {"generatedPassword", member.GetAdditionalDataValueIgnoreCase("GeneratedPassword", null)}, + {"newPassword", member.GetAdditionalDataValueIgnoreCase("NewPassword", null)}, + }, + //TODO: Hard coding this because the changepassword doesn't necessarily need to be a resolvable (real) property editor + View = "changepassword", + //initialize the dictionary with the configuration from the default membership provider + Config = new Dictionary(membersProvider.GetConfiguration(_userService)) + { + //the password change toggle will only be displayed if there is already a password assigned. + {"hasPassword", member.RawPasswordValue.IsNullOrWhiteSpace() == false} + } + }, + new ContentPropertyDisplay + { + Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}membergroup", + Label = _localizedTextService.Localize("content/membergroup"), + Value = GetMemberGroupValue(member.Username), + View = "membergroups", + Config = new Dictionary {{"IsRequired", true}} + } + }; + + return genericProperties; + } + + /// + /// Overridden to assign the IsSensitive property values + /// + /// + /// + /// + /// + protected override List MapProperties(UmbracoContext umbracoContext, IContentBase content, List properties) + { + var result = base.MapProperties(umbracoContext, content, properties); + var member = (IMember)content; + var memberType = member.ContentType; + + //now update the IsSensitive value + foreach (var prop in result) + { + //check if this property is flagged as sensitive + var isSensitiveProperty = memberType.IsSensitiveProperty(prop.Alias); + //check permissions for viewing sensitive data + if (isSensitiveProperty && umbracoContext.Security.CurrentUser.HasAccessToSensitiveData() == false) + { + //mark this property as sensitive + prop.IsSensitive = true; + //mark this property as readonly so that it does not post any data + prop.Readonly = true; + //replace this editor with a sensitivevalue + prop.View = "sensitivevalue"; + //clear the value + prop.Value = null; + } + } + return result; + } + + /// + /// Returns the login property display field + /// + /// + /// + /// + /// + /// + /// + /// If the membership provider installed is the umbraco membership provider, then we will allow changing the username, however if + /// the membership provider is a custom one, we cannot allow chaning the username because MembershipProvider's do not actually natively + /// allow that. + /// + internal static ContentPropertyDisplay GetLoginProperty(IMemberService memberService, IMember member, ILocalizedTextService localizedText) + { + var prop = new ContentPropertyDisplay + { + Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}login", + Label = localizedText.Localize("login"), + Value = member.Username + }; + + var scenario = memberService.GetMembershipScenario(); + + //only allow editing if this is a new member, or if the membership provider is the umbraco one + if (member.HasIdentity == false || scenario == MembershipScenario.NativeUmbraco) + { + prop.View = "textbox"; + prop.Validation.Mandatory = true; + } + else + { + prop.View = "readonlyvalue"; + } + return prop; + } + + internal static IDictionary GetMemberGroupValue(string username) + { + var userRoles = username.IsNullOrWhiteSpace() ? null : Roles.GetRolesForUser(username); + + // create a dictionary of all roles (except internal roles) + "false" + var result = Roles.GetAllRoles().Distinct() + // if a role starts with __umbracoRole we won't show it as it's an internal role used for public access + .Where(x => x.StartsWith(Constants.Conventions.Member.InternalRolePrefix) == false) + .ToDictionary(x => x, x => false); + + // if user has no roles, just return the dictionary + if (userRoles == null) return result; + + // else update the dictionary to "true" for the user roles (except internal roles) + foreach (var userRole in userRoles.Where(x => x.StartsWith(Constants.Conventions.Member.InternalRolePrefix) == false)) + result[userRole] = true; + + return result; } } } diff --git a/src/Umbraco.Web/Models/Mapping/MemberTreeNodeUrlResolver.cs b/src/Umbraco.Web/Models/Mapping/MemberTreeNodeUrlResolver.cs new file mode 100644 index 0000000000..864fd18ab2 --- /dev/null +++ b/src/Umbraco.Web/Models/Mapping/MemberTreeNodeUrlResolver.cs @@ -0,0 +1,23 @@ +using System.Web.Mvc; +using AutoMapper; +using Umbraco.Core.Models; +using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.Trees; + +namespace Umbraco.Web.Models.Mapping +{ + /// + /// Gets the tree node url for the IMember + /// + internal class MemberTreeNodeUrlResolver : IValueResolver + { + public string Resolve(IMember source, MemberDisplay destination, string destMember, ResolutionContext context) + { + var umbracoContext = context.GetUmbracoContext(throwIfMissing: false); + if (umbracoContext == null) return null; + + var urlHelper = new UrlHelper(umbracoContext.HttpContext.Request.RequestContext); + return urlHelper.GetUmbracoApiService(controller => controller.GetTreeNode(source.Key.ToString("N"), null)); + } + } +} diff --git a/src/Umbraco.Web/Models/Mapping/MembershipScenarioResolver.cs b/src/Umbraco.Web/Models/Mapping/MembershipScenarioResolver.cs index 98ff68c578..3c3e0af5aa 100644 --- a/src/Umbraco.Web/Models/Mapping/MembershipScenarioResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/MembershipScenarioResolver.cs @@ -9,9 +9,9 @@ namespace Umbraco.Web.Models.Mapping { internal class MembershipScenarioResolver { - private readonly Lazy _memberTypeService; + private readonly IMemberTypeService _memberTypeService; - public MembershipScenarioResolver(Lazy memberTypeService) + public MembershipScenarioResolver(IMemberTypeService memberTypeService) { _memberTypeService = memberTypeService; } @@ -24,7 +24,7 @@ namespace Umbraco.Web.Models.Mapping { return MembershipScenario.NativeUmbraco; } - var memberType = _memberTypeService.Value.Get(Constants.Conventions.MemberTypes.DefaultAlias); + var memberType = _memberTypeService.Get(Constants.Conventions.MemberTypes.DefaultAlias); return memberType != null ? MembershipScenario.CustomProviderWithUmbracoLink : MembershipScenario.StandaloneCustomProvider; diff --git a/src/Umbraco.Web/Models/Mapping/MembershipUserTypeConverter.cs b/src/Umbraco.Web/Models/Mapping/MembershipUserTypeConverter.cs new file mode 100644 index 0000000000..dbbf6f69df --- /dev/null +++ b/src/Umbraco.Web/Models/Mapping/MembershipUserTypeConverter.cs @@ -0,0 +1,21 @@ +using System.Web.Security; +using AutoMapper; +using Umbraco.Core.Models; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.Web.Models.Mapping +{ + /// + /// A converter to go from a to a + /// + internal class MembershipUserTypeConverter : ITypeConverter + { + public MemberDisplay Convert(MembershipUser source, MemberDisplay destination, ResolutionContext context) + { + //first convert to IMember + var member = Mapper.Map(source); + //then convert to MemberDisplay + return ContextMapper.Map(member, context.GetUmbracoContext()); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/Mapping/DashboardMapperProfile.cs b/src/Umbraco.Web/Models/Mapping/RedirectUrlMapperProfile.cs similarity index 65% rename from src/Umbraco.Web/Models/Mapping/DashboardMapperProfile.cs rename to src/Umbraco.Web/Models/Mapping/RedirectUrlMapperProfile.cs index aa2608875b..e92e72db77 100644 --- a/src/Umbraco.Web/Models/Mapping/DashboardMapperProfile.cs +++ b/src/Umbraco.Web/Models/Mapping/RedirectUrlMapperProfile.cs @@ -1,18 +1,16 @@ using AutoMapper; -using Umbraco.Web.Models.ContentEditing; using Umbraco.Core.Models; +using Umbraco.Web.Composing; +using Umbraco.Web.Models.ContentEditing; namespace Umbraco.Web.Models.Mapping { - /// - /// A model mapper used to map models for the various dashboards - /// - internal class DashboardMapperProfile : Profile + internal class RedirectUrlMapperProfile : Profile { - public DashboardMapperProfile() + public RedirectUrlMapperProfile() { CreateMap() - .ForMember(x => x.OriginalUrl, expression => expression.MapFrom(item => UmbracoContext.Current.UrlProvider.GetUrlFromRoute(item.ContentId, item.Url))) + .ForMember(x => x.OriginalUrl, expression => expression.MapFrom(item => Current.UmbracoContext.UrlProvider.GetUrlFromRoute(item.ContentId, item.Url))) .ForMember(x => x.DestinationUrl, expression => expression.Ignore()) .ForMember(x => x.RedirectId, expression => expression.MapFrom(item => item.Key)); } diff --git a/src/Umbraco.Web/Models/Mapping/TabsAndPropertiesResolver.cs b/src/Umbraco.Web/Models/Mapping/TabsAndPropertiesResolver.cs index 696837c669..cf596c1761 100644 --- a/src/Umbraco.Web/Models/Mapping/TabsAndPropertiesResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/TabsAndPropertiesResolver.cs @@ -10,17 +10,14 @@ using Umbraco.Web.Composing; namespace Umbraco.Web.Models.Mapping { - /// - /// Creates the tabs collection with properties assigned for display models - /// - internal class TabsAndPropertiesResolver + internal abstract class TabsAndPropertiesResolver { - private readonly ILocalizedTextService _localizedTextService; + protected ILocalizedTextService LocalizedTextService { get; } protected IEnumerable IgnoreProperties { get; set; } public TabsAndPropertiesResolver(ILocalizedTextService localizedTextService) { - _localizedTextService = localizedTextService ?? throw new ArgumentNullException(nameof(localizedTextService)); + LocalizedTextService = localizedTextService ?? throw new ArgumentNullException(nameof(localizedTextService)); IgnoreProperties = new List(); } @@ -29,87 +26,7 @@ namespace Umbraco.Web.Models.Mapping { IgnoreProperties = ignoreProperties ?? throw new ArgumentNullException(nameof(ignoreProperties)); } - - /// - /// Maps properties on to the generic properties tab - /// - /// - /// - /// - /// - /// Any additional custom properties to assign to the generic properties tab. - /// - /// - /// - /// The generic properties tab is mapped during AfterMap and is responsible for - /// setting up the properties such as Created date, updated date, template selected, etc... - /// - public static void MapGenericProperties( - TPersisted content, - ContentItemDisplayBase display, - ILocalizedTextService localizedTextService, - IEnumerable customProperties = null, - Action> onGenericPropertiesMapped = null) - where TPersisted : IContentBase - { - var genericProps = display.Tabs.Single(x => x.Id == 0); - - //store the current props to append to the newly inserted ones - var currProps = genericProps.Properties.ToArray(); - - var labelEditor = Current.PropertyEditors[Constants.PropertyEditors.Aliases.NoEdit].GetValueEditor().View; - - var contentProps = new List - { - new ContentPropertyDisplay - { - Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}id", - Label = "Id", - Value = Convert.ToInt32(display.Id).ToInvariantString() + "
" + display.Key + "", - View = labelEditor - }, - new ContentPropertyDisplay - { - Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}creator", - Label = localizedTextService.Localize("content/createBy"), - Description = localizedTextService.Localize("content/createByDesc"), - Value = display.Owner.Name, - View = labelEditor - }, - new ContentPropertyDisplay - { - Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}createdate", - Label = localizedTextService.Localize("content/createDate"), - Description = localizedTextService.Localize("content/createDateDesc"), - Value = display.CreateDate.ToIsoString(), - View = labelEditor - }, - new ContentPropertyDisplay - { - Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}updatedate", - Label = localizedTextService.Localize("content/updateDate"), - Description = localizedTextService.Localize("content/updateDateDesc"), - Value = display.UpdateDate.ToIsoString(), - View = labelEditor - } - }; - - if (customProperties != null) - { - //add the custom ones - contentProps.AddRange(customProperties); - } - - //now add the user props - contentProps.AddRange(currProps); - - //callback - onGenericPropertiesMapped?.Invoke(contentProps); - - //re-assign - genericProps.Properties = contentProps; - } - + /// /// Adds the container (listview) tab to the document /// @@ -136,7 +53,7 @@ namespace Umbraco.Web.Models.Mapping dtdId = Constants.DataTypes.DefaultMembersListView; break; default: - throw new ArgumentOutOfRangeException("entityType does not match a required value"); + throw new ArgumentOutOfRangeException(nameof(entityType), "entityType does not match a required value"); } //first try to get the custom one if there is one @@ -220,15 +137,117 @@ namespace Umbraco.Web.Models.Mapping display.Tabs = tabs; } - public virtual IEnumerable> Resolve(IContentBase content) + /// + /// Returns a collection of custom generic properties that exist on the generic properties tab + /// + /// + protected virtual IEnumerable GetCustomGenericProperties(IContentBase content) { + return Enumerable.Empty(); + } + + /// + /// Maps properties on to the generic properties tab + /// + /// + /// + /// + /// + /// The generic properties tab is responsible for + /// setting up the properties such as Created date, updated date, template selected, etc... + /// + protected virtual void MapGenericProperties(UmbracoContext umbracoContext, IContentBase content, List> tabs) + { + // add the generic properties tab, for properties that don't belong to a tab + // get the properties, map and translate them, then add the tab + var noGroupProperties = content.GetNonGroupedProperties() + .Where(x => IgnoreProperties.Contains(x.Alias) == false) // skip ignored + .ToList(); + var genericproperties = MapProperties(umbracoContext, content, noGroupProperties); + + tabs.Add(new Tab + { + Id = 0, + Label = LocalizedTextService.Localize("general/properties"), + Alias = "Generic properties", + Properties = genericproperties + }); + + var genericProps = tabs.Single(x => x.Id == 0); + + //store the current props to append to the newly inserted ones + var currProps = genericProps.Properties.ToArray(); + + var contentProps = new List(); + + var customProperties = GetCustomGenericProperties(content); + if (customProperties != null) + { + //add the custom ones + contentProps.AddRange(customProperties); + } + + //now add the user props + contentProps.AddRange(currProps); + + //re-assign + genericProps.Properties = contentProps; + + //Show or hide properties tab based on wether it has or not any properties + if (genericProps.Properties.Any() == false) + { + //loop throug the tabs, remove the one with the id of zero and exit the loop + for (var i = 0; i < tabs.Count; i++) + { + if (tabs[i].Id != 0) continue; + tabs.RemoveAt(i); + break; + } + } + } + + /// + /// Maps a list of to a list of + /// + /// + /// + /// + /// + protected virtual List MapProperties(UmbracoContext umbracoContext, IContentBase content, List properties) + { + var result = Mapper.Map, IEnumerable>( + // Sort properties so items from different compositions appear in correct order (see U4-9298). Map sorted properties. + properties.OrderBy(prop => prop.PropertyType.SortOrder)) + .ToList(); + + return result; + } + } + + /// + /// Creates the tabs collection with properties assigned for display models + /// + internal class TabsAndPropertiesResolver : TabsAndPropertiesResolver, IValueResolver>> + where TSource : IContentBase + { + public TabsAndPropertiesResolver(ILocalizedTextService localizedTextService) + : base(localizedTextService) + { } + + public TabsAndPropertiesResolver(ILocalizedTextService localizedTextService, IEnumerable ignoreProperties) + : base(localizedTextService, ignoreProperties) + { } + + public virtual IEnumerable> Resolve(TSource source, TDestination destination, IEnumerable> destMember, ResolutionContext context) + { + var umbracoContext = context.GetUmbracoContext(); var tabs = new List>(); // add the tabs, for properties that belong to a tab // need to aggregate the tabs, as content.PropertyGroups contains all the composition tabs, // and there might be duplicates (content does not work like contentType and there is no // content.CompositionPropertyGroups). - var groupsGroupsByName = content.PropertyGroups.OrderBy(x => x.SortOrder).GroupBy(x => x.Name); + var groupsGroupsByName = source.PropertyGroups.OrderBy(x => x.SortOrder).GroupBy(x => x.Name); foreach (var groupsByName in groupsGroupsByName) { var properties = new List(); @@ -236,7 +255,7 @@ namespace Umbraco.Web.Models.Mapping // merge properties for groups with the same name foreach (var group in groupsByName) { - var groupProperties = content.GetPropertiesForGroup(group) + var groupProperties = source.GetPropertiesForGroup(group) .Where(x => IgnoreProperties.Contains(x.Alias) == false); // skip ignored properties.AddRange(groupProperties); @@ -245,14 +264,12 @@ namespace Umbraco.Web.Models.Mapping if (properties.Count == 0) continue; - // Sort properties so items from different compositions appear in correct order (see U4-9298). Map sorted properties. - var mappedProperties = Mapper.Map, IEnumerable>(properties.OrderBy(prop => prop.PropertyType.SortOrder)); - - TranslateProperties(mappedProperties); + //map the properties + var mappedProperties = MapProperties(umbracoContext, source, properties); // add the tab // we need to pick an identifier... there is no "right" way... - var g = groupsByName.FirstOrDefault(x => x.Id == content.ContentTypeId) // try local + var g = groupsByName.FirstOrDefault(x => x.Id == source.ContentTypeId) // try local ?? groupsByName.First(); // else pick one randomly var groupId = g.Id; var groupName = groupsByName.Key; @@ -260,41 +277,19 @@ namespace Umbraco.Web.Models.Mapping { Id = groupId, Alias = groupName, - Label = _localizedTextService.UmbracoDictionaryTranslate(groupName), + Label = LocalizedTextService.UmbracoDictionaryTranslate(groupName), Properties = mappedProperties, IsActive = false }); } - // add the generic properties tab, for properties that don't belong to a tab - // get the properties, map and translate them, then add the tab - var noGroupProperties = content.GetNonGroupedProperties() - .Where(x => IgnoreProperties.Contains(x.Alias) == false); // skip ignored - var genericproperties = Mapper.Map, IEnumerable>(noGroupProperties).ToList(); - TranslateProperties(genericproperties); + MapGenericProperties(umbracoContext, source, tabs); - tabs.Add(new Tab - { - Id = 0, - Label = _localizedTextService.Localize("general/properties"), - Alias = "Generic properties", - Properties = genericproperties - }); - - // activate the first tab - tabs.First().IsActive = true; + // activate the first tab, if any + if (tabs.Count > 0) + tabs[0].IsActive = true; return tabs; } - - private void TranslateProperties(IEnumerable properties) - { - // Not sure whether it's a good idea to add this to the ContentPropertyDisplay mapper - foreach (var prop in properties) - { - prop.Label = _localizedTextService.UmbracoDictionaryTranslate(prop.Label); - prop.Description = _localizedTextService.UmbracoDictionaryTranslate(prop.Description); - } - } } } diff --git a/src/Umbraco.Web/Models/Mapping/UserMapperProfile.cs b/src/Umbraco.Web/Models/Mapping/UserMapperProfile.cs index f02defa031..83a2fffdb2 100644 --- a/src/Umbraco.Web/Models/Mapping/UserMapperProfile.cs +++ b/src/Umbraco.Web/Models/Mapping/UserMapperProfile.cs @@ -26,7 +26,7 @@ namespace Umbraco.Web.Models.Mapping var userGroupDefaultPermissionsResolver = new UserGroupDefaultPermissionsResolver(textService, actions); CreateMap() - .ConstructUsing((UserGroupSave save) => new UserGroup { CreateDate = DateTime.Now }) + .ConstructUsing(save => new UserGroup { CreateDate = DateTime.UtcNow }) .IgnoreEntityCommonProperties() .ForMember(dest => dest.Id, opt => opt.Condition(source => GetIntId(source.Id) > 0)) .ForMember(dest => dest.Id, opt => opt.MapFrom(source => GetIntId(source.Id))) @@ -44,6 +44,7 @@ namespace Umbraco.Web.Models.Mapping CreateMap() .IgnoreEntityCommonProperties() .ForMember(dest => dest.Id, opt => opt.Condition(src => GetIntId(src.Id) > 0)) + .ForMember(detail => detail.TourData, opt => opt.Ignore()) .ForMember(dest => dest.SessionTimeout, opt => opt.Ignore()) .ForMember(dest => dest.EmailConfirmedDate, opt => opt.Ignore()) .ForMember(dest => dest.UserType, opt => opt.Ignore()) @@ -75,6 +76,7 @@ namespace Umbraco.Web.Models.Mapping CreateMap() .IgnoreEntityCommonProperties() .ForMember(dest => dest.Id, opt => opt.Ignore()) + .ForMember(detail => detail.TourData, opt => opt.Ignore()) .ForMember(dest => dest.StartContentIds, opt => opt.Ignore()) .ForMember(dest => dest.StartMediaIds, opt => opt.Ignore()) .ForMember(dest => dest.UserType, opt => opt.Ignore()) @@ -200,9 +202,22 @@ namespace Umbraco.Web.Models.Mapping var allContentPermissions = userService.GetPermissions(@group, true) .ToDictionary(x => x.EntityId, x => x); - var contentEntities = allContentPermissions.Keys.Count == 0 - ? Array.Empty() - : entityService.GetAll(UmbracoObjectTypes.Document, allContentPermissions.Keys.ToArray()); + IEntitySlim[] contentEntities; + if (allContentPermissions.Keys.Count == 0) + { + contentEntities = Array.Empty(); + } + else + { + // a group can end up with way more than 2000 assigned permissions, + // so we need to break them into groups in order to avoid breaking + // the entity service due to too many Sql parameters. + + var list = new List(); + foreach (var idGroup in allContentPermissions.Keys.InGroupsOf(2000)) + list.AddRange(entityService.GetAll(UmbracoObjectTypes.Document, idGroup.ToArray())); + contentEntities = list.ToArray(); + } var allAssignedPermissions = new List(); foreach (var entity in contentEntities) @@ -229,10 +244,11 @@ namespace Umbraco.Web.Models.Mapping //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.Avatars, opt => opt.MapFrom(user => user.GetUserAvatarUrls(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(detail => detail.Navigation, opt => opt.MapFrom(user => CreateUserEditorNavigation(textService))) .ForMember( dest => dest.CalculatedStartContentIds, opt => opt.MapFrom(src => GetStartNodeValues( @@ -278,7 +294,7 @@ namespace Umbraco.Web.Models.Mapping //like the load time is waiting. .ForMember(detail => detail.Avatars, - opt => opt.MapFrom(user => user.GetCurrentUserAvatarUrls(userService, runtimeCache))) + opt => opt.MapFrom(user => user.GetUserAvatarUrls(runtimeCache))) .ForMember(dest => dest.Username, opt => opt.MapFrom(user => user.Username)) .ForMember(dest => dest.UserGroups, opt => opt.MapFrom(user => user.Groups)) .ForMember(dest => dest.LastLoginDate, opt => opt.MapFrom(user => user.LastLoginDate == default(DateTime) ? null : (DateTime?)user.LastLoginDate)) @@ -298,7 +314,7 @@ namespace Umbraco.Web.Models.Mapping .ForMember(dest => dest.AdditionalData, opt => opt.Ignore()); CreateMap() - .ForMember(dest => dest.Avatars, opt => opt.MapFrom(user => user.GetCurrentUserAvatarUrls(userService, runtimeCache))) + .ForMember(dest => dest.Avatars, opt => opt.MapFrom(user => user.GetUserAvatarUrls(runtimeCache))) .ForMember(dest => dest.UserId, opt => opt.MapFrom(user => GetIntId(user.Id))) .ForMember(dest => dest.StartContentIds, opt => opt.MapFrom(user => user.CalculateContentStartNodeIds(entityService))) .ForMember(dest => dest.StartMediaIds, opt => opt.MapFrom(user => user.CalculateMediaStartNodeIds(entityService))) @@ -340,18 +356,21 @@ namespace Umbraco.Web.Models.Mapping CreateMap() .ForMember(dest => dest.UserId, opt => opt.MapFrom(profile => GetIntId(profile.Id))); + } - CreateMap() - .ConstructUsing((IUser user) => new UserData()) - .ForMember(dest => dest.Id, opt => opt.MapFrom(user => user.Id)) - .ForMember(dest => dest.AllowedApplications, opt => opt.MapFrom(user => user.AllowedSections.ToArray())) - .ForMember(dest => dest.RealName, opt => opt.MapFrom(user => user.Name)) - .ForMember(dest => dest.Roles, opt => opt.MapFrom(user => user.Groups.Select(x => x.Alias).ToArray())) - .ForMember(dest => dest.StartContentNodes, opt => opt.MapFrom(user => user.CalculateContentStartNodeIds(entityService))) - .ForMember(dest => dest.StartMediaNodes, opt => opt.MapFrom(user => user.CalculateMediaStartNodeIds(entityService))) - .ForMember(dest => dest.Username, opt => opt.MapFrom(user => user.Username)) - .ForMember(dest => dest.Culture, opt => opt.MapFrom(user => user.GetUserCulture(textService))) - .ForMember(dest => dest.SessionId, opt => opt.MapFrom(user => user.SecurityStamp.IsNullOrWhiteSpace() ? Guid.NewGuid().ToString("N") : user.SecurityStamp)); + private IEnumerable CreateUserEditorNavigation(ILocalizedTextService textService) + { + return new[] + { + new EditorNavigation + { + Active = true, + Alias = "details", + Icon = "icon-umb-users", + Name = textService.Localize("general/user"), + View = "views/users/views/user/details.html" + } + }; } private IEnumerable GetStartNodeValues(int[] startNodeIds, diff --git a/src/Umbraco.Web/Models/Trees/ExportMember.cs b/src/Umbraco.Web/Models/Trees/ExportMember.cs new file mode 100644 index 0000000000..66a10da007 --- /dev/null +++ b/src/Umbraco.Web/Models/Trees/ExportMember.cs @@ -0,0 +1,9 @@ +namespace Umbraco.Web.Models.Trees +{ + /// + /// Represents the export member menu item + /// + [ActionMenuItem("umbracoMenuActions")] + public sealed class ExportMember : ActionMenuItem + { } +} diff --git a/src/Umbraco.Web/Models/Trees/MenuItem.cs b/src/Umbraco.Web/Models/Trees/MenuItem.cs index 43da07f11e..003743043b 100644 --- a/src/Umbraco.Web/Models/Trees/MenuItem.cs +++ b/src/Umbraco.Web/Models/Trees/MenuItem.cs @@ -192,7 +192,7 @@ namespace Umbraco.Web.Models.Trees else { // if that doesn't work, try to get the legacy confirm view - var attempt2 = LegacyTreeDataConverter.GetLegacyConfirmView(Action, currentSection); + var attempt2 = LegacyTreeDataConverter.GetLegacyConfirmView(Action); if (attempt2) { var view = attempt2.Result; diff --git a/src/Umbraco.Web/Models/Trees/MenuItemList.cs b/src/Umbraco.Web/Models/Trees/MenuItemList.cs index 26fb0d2796..2e38939f1e 100644 --- a/src/Umbraco.Web/Models/Trees/MenuItemList.cs +++ b/src/Umbraco.Web/Models/Trees/MenuItemList.cs @@ -30,7 +30,7 @@ namespace Umbraco.Web.Models.Trees /// The text to display for the menu item, will default to the IAction alias if not specified internal MenuItem Add(IAction action, string name) { - var item = new MenuItem(action); + var item = new MenuItem(action, name); DetectLegacyActionMenu(action.GetType(), item); diff --git a/src/Umbraco.Web/Models/UserTourStatus.cs b/src/Umbraco.Web/Models/UserTourStatus.cs new file mode 100644 index 0000000000..d1834f3d6b --- /dev/null +++ b/src/Umbraco.Web/Models/UserTourStatus.cs @@ -0,0 +1,60 @@ +using System; +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models +{ + /// + /// A model representing the tours a user has taken/completed + /// + [DataContract(Name = "userTourStatus", Namespace = "")] + public class UserTourStatus : IEquatable + { + /// + /// The tour alias + /// + [DataMember(Name = "alias")] + public string Alias { get; set; } + + /// + /// If the tour is completed + /// + [DataMember(Name = "completed")] + public bool Completed { get; set; } + + /// + /// If the tour is disabled + /// + [DataMember(Name = "disabled")] + public bool Disabled { get; set; } + + public bool Equals(UserTourStatus other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return string.Equals(Alias, other.Alias); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((UserTourStatus) obj); + } + + public override int GetHashCode() + { + return Alias.GetHashCode(); + } + + public static bool operator ==(UserTourStatus left, UserTourStatus right) + { + return Equals(left, right); + } + + public static bool operator !=(UserTourStatus left, UserTourStatus right) + { + return !Equals(left, right); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Mvc/ContentModelBinder.cs b/src/Umbraco.Web/Mvc/ContentModelBinder.cs index 33ea5f84f8..b68d5a36f7 100644 --- a/src/Umbraco.Web/Mvc/ContentModelBinder.cs +++ b/src/Umbraco.Web/Mvc/ContentModelBinder.cs @@ -16,6 +16,11 @@ namespace Umbraco.Web.Mvc /// public class ContentModelBinder : DefaultModelBinder, IModelBinderProvider { + // use Instance + private ContentModelBinder() { } + + public static ContentModelBinder Instance = new ContentModelBinder(); + /// /// Binds the model to a value by using the specified controller context and binding context. /// @@ -119,6 +124,7 @@ namespace Umbraco.Web.Mvc { var msg = new StringBuilder(); + // prepare message msg.Append("Cannot bind source"); if (sourceContent) msg.Append(" content"); msg.Append(" type "); @@ -129,22 +135,17 @@ namespace Umbraco.Web.Mvc msg.Append(modelType.FullName); msg.Append("."); - // compare FullName for the time being because when upgrading ModelsBuilder, - // Umbraco does not know about the new attribute type - later on, can compare - // on type directly (ie after v7.4.2). - var sourceAttr = sourceType.Assembly.CustomAttributes.FirstOrDefault(x => - x.AttributeType.FullName == "Umbraco.ModelsBuilder.PureLiveAssemblyAttribute"); - var modelAttr = modelType.Assembly.CustomAttributes.FirstOrDefault(x => - x.AttributeType.FullName == "Umbraco.ModelsBuilder.PureLiveAssemblyAttribute"); +// raise event, to give model factories a chance at reporting + // the error with more details, and optionally request that + // the application restarts. - // bah.. names are App_Web_all.generated.cs.8f9494c4.jjuvxz55 so they ARE different, fuck! - // we cannot compare purely on type.FullName 'cos we might be trying to map Sub to Main = fails! - if (sourceAttr != null && modelAttr != null - && sourceType.Assembly.GetName().Version.Revision != modelType.Assembly.GetName().Version.Revision) + var args = new ModelBindingArgs(sourceType, modelType, msg); + ModelBindingException?.Invoke(Instance, args); + + if (args.Restart) { - msg.Append(" Types come from two PureLive assemblies with different versions,"); - msg.Append(" this usually indicates that the application is in an unstable state."); - msg.Append(" The application is restarting now, reload the page and it should work."); + msg.Append(" The application is restarting now."); + var context = HttpContext.Current; if (context == null) AppDomain.Unload(AppDomain.CurrentDomain); @@ -167,5 +168,47 @@ namespace Umbraco.Web.Mvc if (typeof(IPublishedContent).IsAssignableFrom(modelType)) return this; return null; } + + /// + /// Contains event data for the event. + /// + public class ModelBindingArgs : EventArgs + { + /// + /// Initializes a new instance of the class. + /// + public ModelBindingArgs(Type sourceType, Type modelType, StringBuilder message) + { + SourceType = sourceType; + ModelType = modelType; + Message = message; + } + + /// + /// Gets the type of the source object. + /// + public Type SourceType { get; set; } + + /// + /// Gets the type of the view model. + /// + public Type ModelType { get; set; } + + /// + /// Gets the message string builder. + /// + /// Handlers of the event can append text to the message. + public StringBuilder Message { get; } + + /// + /// Gets or sets a value indicating whether the application should restart. + /// + public bool Restart { get; set; } + } + + /// + /// Occurs on model binding exceptions. + /// + public static event EventHandler ModelBindingException; } } diff --git a/src/Umbraco.Web/Mvc/RenderRouteHandler.cs b/src/Umbraco.Web/Mvc/RenderRouteHandler.cs index b9446d3aa7..b9bb71c42c 100644 --- a/src/Umbraco.Web/Mvc/RenderRouteHandler.cs +++ b/src/Umbraco.Web/Mvc/RenderRouteHandler.cs @@ -33,10 +33,8 @@ namespace Umbraco.Web.Mvc // fixme - that one could / should accept a PublishedRouter (engine) to work on the PublishedRequest (published content request) public RenderRouteHandler(IUmbracoContextAccessor umbracoContextAccessor, IControllerFactory controllerFactory) { - if (umbracoContextAccessor == null) throw new ArgumentNullException(nameof(umbracoContextAccessor)); - if (controllerFactory == null) throw new ArgumentNullException(nameof(controllerFactory)); - _umbracoContextAccessor = umbracoContextAccessor; - _controllerFactory = controllerFactory; + _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); + _controllerFactory = controllerFactory ?? throw new ArgumentNullException(nameof(controllerFactory)); } // fixme - what about that one? @@ -45,10 +43,8 @@ namespace Umbraco.Web.Mvc // UmbracoComponentRenderer - ?? that one is not so obvious public RenderRouteHandler(UmbracoContext umbracoContext, IControllerFactory controllerFactory) { - if (umbracoContext == null) throw new ArgumentNullException(nameof(umbracoContext)); - if (controllerFactory == null) throw new ArgumentNullException(nameof(controllerFactory)); - _umbracoContext = umbracoContext; - _controllerFactory = controllerFactory; + _umbracoContext = umbracoContext ?? throw new ArgumentNullException(nameof(umbracoContext)); + _controllerFactory = controllerFactory ?? throw new ArgumentNullException(nameof(controllerFactory)); } private UmbracoContext UmbracoContext => _umbracoContext ?? _umbracoContextAccessor.UmbracoContext; @@ -65,12 +61,12 @@ namespace Umbraco.Web.Mvc { if (UmbracoContext == null) { - throw new NullReferenceException("There is not current UmbracoContext, it must be initialized before the RenderRouteHandler executes"); + throw new NullReferenceException("There is no current UmbracoContext, it must be initialized before the RenderRouteHandler executes"); } var request = UmbracoContext.PublishedRequest; if (request == null) { - throw new NullReferenceException("There is not current PublishedContentRequest, it must be initialized before the RenderRouteHandler executes"); + throw new NullReferenceException("There is no current PublishedContentRequest, it must be initialized before the RenderRouteHandler executes"); } SetupRouteDataForRequest( @@ -394,7 +390,7 @@ namespace Umbraco.Web.Mvc //here we need to check if there is no hijacked route and no template assigned, if this is the case //we want to return a blank page, but we'll leave that up to the NoTemplateHandler. - if (request.HasTemplate == false && routeDef.HasHijackedRoute == false) + if (!request.HasTemplate && !routeDef.HasHijackedRoute && !_features.Enabled.RenderNoTemplate) { // fixme - better find a way to inject that engine? or at least Current.Engine of some sort! var engine = Core.Composing.Current.Container.GetInstance(); @@ -455,7 +451,5 @@ namespace Umbraco.Web.Mvc { return _controllerFactory.GetControllerSessionBehavior(requestContext, controllerName); } - - } } diff --git a/src/Umbraco.Web/Properties/AssemblyInfo.cs b/src/Umbraco.Web/Properties/AssemblyInfo.cs index d1d9e10597..b7d046787e 100644 --- a/src/Umbraco.Web/Properties/AssemblyInfo.cs +++ b/src/Umbraco.Web/Properties/AssemblyInfo.cs @@ -33,6 +33,9 @@ using System.Runtime.InteropServices; [assembly: InternalsVisibleTo("Umbraco.Forms.Core.Providers")] [assembly: InternalsVisibleTo("Umbraco.Forms.Web")] +// Umbraco Headless +[assembly: InternalsVisibleTo("Umbraco.Headless")] + // v8 [assembly: InternalsVisibleTo("Umbraco.Compat7")] diff --git a/src/Umbraco.Web/PublishedContentQuery.cs b/src/Umbraco.Web/PublishedContentQuery.cs index 2e48bf1c55..2a8bedc79f 100644 --- a/src/Umbraco.Web/PublishedContentQuery.cs +++ b/src/Umbraco.Web/PublishedContentQuery.cs @@ -1,7 +1,11 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Xml.XPath; +using Examine.LuceneEngine.Providers; +using Examine.LuceneEngine.SearchCriteria; +using Examine.SearchCriteria; using Umbraco.Core; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Xml; @@ -27,10 +31,8 @@ namespace Umbraco.Web /// public PublishedContentQuery(IPublishedContentCache contentCache, IPublishedMediaCache mediaCache) { - if (contentCache == null) throw new ArgumentNullException(nameof(contentCache)); - if (mediaCache == null) throw new ArgumentNullException(nameof(mediaCache)); - _contentCache = contentCache; - _mediaCache = mediaCache; + _contentCache = contentCache ?? throw new ArgumentNullException(nameof(contentCache)); + _mediaCache = mediaCache ?? throw new ArgumentNullException(nameof(mediaCache)); } /// @@ -39,8 +41,7 @@ namespace Umbraco.Web /// public PublishedContentQuery(IPublishedContentQuery query) { - if (query == null) throw new ArgumentNullException(nameof(query)); - _query = query; + _query = query ?? throw new ArgumentNullException(nameof(query)); } #region Content @@ -217,43 +218,104 @@ namespace Umbraco.Web #region Search - /// - /// Searches content - /// - /// - /// - /// - /// + /// public IEnumerable Search(string term, bool useWildCards = true, string searchProvider = null) { - if (_query != null) return _query.Search(term, useWildCards, searchProvider); + return Search(0, 0, out _, term, useWildCards, searchProvider); + } - var searcher = Examine.ExamineManager.Instance.DefaultSearchProvider; - if (string.IsNullOrEmpty(searchProvider) == false) - searcher = Examine.ExamineManager.Instance.SearchProviderCollection[searchProvider]; + /// + public IEnumerable Search(int skip, int take, out int totalRecords, string term, bool useWildCards = true, string searchProvider = null) + { + if (_query != null) return _query.Search(skip, take, out totalRecords, term, useWildCards, searchProvider); - var results = searcher.Search(term, useWildCards); + var searcher = string.IsNullOrWhiteSpace(searchProvider) + ? Examine.ExamineManager.Instance.DefaultSearchProvider + : Examine.ExamineManager.Instance.SearchProviderCollection[searchProvider]; + + if (skip == 0 && take == 0) + { + var results = searcher.Search(term, useWildCards); + totalRecords = results.TotalItemCount; + return results.ToPublishedSearchResults(_contentCache); + } + + if (!(searcher is BaseLuceneSearcher luceneSearcher)) + { + var results = searcher.Search(term, useWildCards); + totalRecords = results.TotalItemCount; + // Examine skip, Linq take + return results.Skip(skip).ToPublishedSearchResults(_contentCache).Take(take); + } + + var criteria = SearchAllFields(term, useWildCards, luceneSearcher); + return Search(skip, take, out totalRecords, criteria, searcher); + } + + /// + public IEnumerable Search(Examine.SearchCriteria.ISearchCriteria criteria, Examine.Providers.BaseSearchProvider searchProvider = null) + { + return Search(0, 0, out _, criteria, searchProvider); + } + + /// + public IEnumerable Search(int skip, int take, out int totalRecords, Examine.SearchCriteria.ISearchCriteria criteria, Examine.Providers.BaseSearchProvider searchProvider = null) + { + if (_query != null) return _query.Search(skip, take, out totalRecords, criteria, searchProvider); + + var searcher = searchProvider ?? Examine.ExamineManager.Instance.DefaultSearchProvider; + + var results = skip == 0 && take == 0 + ? searcher.Search(criteria) + : searcher.Search(criteria, maxResults: skip + take); + + totalRecords = results.TotalItemCount; return results.ToPublishedSearchResults(_contentCache); } /// - /// Searhes content + /// Creates an ISearchCriteria for searching all fields in a . /// - /// - /// - /// - public IEnumerable Search(Examine.SearchCriteria.ISearchCriteria criteria, Examine.Providers.BaseSearchProvider searchProvider = null) + /// + /// This is here because some of this stuff is internal in Examine. + /// + private ISearchCriteria SearchAllFields(string searchText, bool useWildcards, BaseLuceneSearcher searcher) { - if (_query != null) return _query.Search(criteria, searchProvider); + var sc = searcher.CreateSearchCriteria(); - var s = Examine.ExamineManager.Instance.DefaultSearchProvider; - if (searchProvider != null) - s = searchProvider; + if (_examineGetSearchFields == null) + { + //get the GetSearchFields method from BaseLuceneSearcher + _examineGetSearchFields = typeof(BaseLuceneSearcher).GetMethod("GetSearchFields", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static); + } - var results = s.Search(criteria); - return results.ToPublishedSearchResults(_contentCache); + //get the results of searcher.BaseLuceneSearcher() using ugly reflection since it's not public + var searchFields = (IEnumerable) _examineGetSearchFields.Invoke(searcher, null); + + //this is what Examine does internally to create ISearchCriteria for searching all fields + var strArray = searchText.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + sc = useWildcards == false + ? sc.GroupedOr(searchFields, strArray).Compile() + : sc.GroupedOr(searchFields, strArray.Select(x => new CustomExamineValue(Examineness.ComplexWildcard, x.MultipleCharacterWildcard().Value)).ToArray()).Compile(); + return sc; } + private static MethodInfo _examineGetSearchFields; + + //support class since Examine doesn't expose it's own ExamineValue class publicly + private class CustomExamineValue : IExamineValue + { + public CustomExamineValue(Examineness vagueness, string value) + { + this.Examineness = vagueness; + this.Value = value; + this.Level = 1f; + } + public Examineness Examineness { get; private set; } + public string Value { get; private set; } + public float Level { get; private set; } + } + #endregion } } diff --git a/src/Umbraco.Web/Runtime/WebRuntimeComponent.cs b/src/Umbraco.Web/Runtime/WebRuntimeComponent.cs index 6cbed8dc6e..e2a213c954 100644 --- a/src/Umbraco.Web/Runtime/WebRuntimeComponent.cs +++ b/src/Umbraco.Web/Runtime/WebRuntimeComponent.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Configuration; +using System.IO; using System.Linq; using System.Web; using System.Web.Configuration; @@ -9,6 +10,7 @@ using System.Web.Http; using System.Web.Http.Dispatcher; using System.Web.Mvc; using System.Web.Routing; +using ClientDependency.Core.CompositeFiles.Providers; using ClientDependency.Core.Config; using LightInject; using Microsoft.AspNet.SignalR; @@ -41,6 +43,7 @@ using Umbraco.Web.Search; using Umbraco.Web.Security; using Umbraco.Web.Services; using Umbraco.Web.SignalR; +using Umbraco.Web.Tour; using Umbraco.Web.Trees; using Umbraco.Web.UI.JavaScript; using Umbraco.Web.WebApi; @@ -117,6 +120,10 @@ namespace Umbraco.Web.Runtime composition.Container.RegisterCollectionBuilder() .Add(() => typeLoader.GetTypes()); + composition.Container.RegisterCollectionBuilder(); + + composition.Container.RegisterCollectionBuilder(); // fixme FeaturesResolver? + // set the default RenderMvcController Current.DefaultRenderMvcControllerType = typeof(RenderMvcController); // fixme WRONG! @@ -202,23 +209,8 @@ namespace Umbraco.Web.Runtime // setup mvc and webapi services SetupMvcAndWebApi(); - // Backwards compatibility - set the path and URL type for ClientDependency 1.5.1 [LK] - ClientDependency.Core.CompositeFiles.Providers.XmlFileMapper.FileMapVirtualFolder = "~/App_Data/TEMP/ClientDependency"; - ClientDependency.Core.CompositeFiles.Providers.BaseCompositeFileProcessingProvider.UrlTypeDefault = ClientDependency.Core.CompositeFiles.Providers.CompositeUrlType.Base64QueryStrings; - - if (ConfigurationManager.GetSection("system.web/httpRuntime") is HttpRuntimeSection section) - { - //set the max url length for CDF to be the smallest of the max query length, max request length - ClientDependency.Core.CompositeFiles.CompositeDependencyHandler.MaxHandlerUrlLength = Math.Min(section.MaxQueryStringLength, section.MaxRequestLength); - } - - //Register a custom renderer - used to process property editor dependencies - var renderer = new DependencyPathRenderer(); - renderer.Initialize("Umbraco.DependencyPathRenderer", new NameValueCollection - { - { "compositeFileHandlerPath", ClientDependencySettings.Instance.CompositeFileHandlerPath } - }); - ClientDependencySettings.Instance.MvcRendererCollection.Add(renderer); + // client dependency + ConfigureClientDependency(); // Disable the X-AspNetMvc-Version HTTP Header MvcHandler.DisableMvcResponseHeader = true; @@ -372,7 +364,7 @@ namespace Umbraco.Web.Runtime ViewEngines.Engines.Add(new PluginViewEngine()); //set model binder - ModelBinderProviders.BinderProviders.Add(new ContentModelBinder()); // is a provider + ModelBinderProviders.BinderProviders.Add(ContentModelBinder.Instance); // is a provider ////add the profiling action filter //GlobalFilters.Filters.Add(new ProfilingActionFilter()); @@ -380,5 +372,44 @@ namespace Umbraco.Web.Runtime GlobalConfiguration.Configuration.Services.Replace(typeof(IHttpControllerSelector), new NamespaceHttpControllerSelector(GlobalConfiguration.Configuration)); } + + private static void ConfigureClientDependency() + { + // Backwards compatibility - set the path and URL type for ClientDependency 1.5.1 [LK] + XmlFileMapper.FileMapDefaultFolder = "~/App_Data/TEMP/ClientDependency"; + BaseCompositeFileProcessingProvider.UrlTypeDefault = CompositeUrlType.Base64QueryStrings; + + // Now we need to detect if we are running umbracoLocalTempStorage as EnvironmentTemp and in that case we want to change the CDF file + // location to be there + if (GlobalSettings.LocalTempStorageLocation == LocalTempStorage.EnvironmentTemp) + { + var appDomainHash = HttpRuntime.AppDomainAppId.ToSHA1(); + var cachePath = Path.Combine(Environment.ExpandEnvironmentVariables("%temp%"), "UmbracoData", + //include the appdomain hash is just a safety check, for example if a website is moved from worker A to worker B and then back + // to worker A again, in theory the %temp% folder should already be empty but we really want to make sure that its not + // utilizing an old path + appDomainHash); + + //set the file map and composite file default location to the %temp% location + BaseCompositeFileProcessingProvider.CompositeFilePathDefaultFolder + = XmlFileMapper.FileMapDefaultFolder + = Path.Combine(cachePath, "ClientDependency"); + } + + if (ConfigurationManager.GetSection("system.web/httpRuntime") is HttpRuntimeSection section) + { + //set the max url length for CDF to be the smallest of the max query length, max request length + ClientDependency.Core.CompositeFiles.CompositeDependencyHandler.MaxHandlerUrlLength = Math.Min(section.MaxQueryStringLength, section.MaxRequestLength); + } + + //Register a custom renderer - used to process property editor dependencies + var renderer = new DependencyPathRenderer(); + renderer.Initialize("Umbraco.DependencyPathRenderer", new NameValueCollection + { + { "compositeFileHandlerPath", ClientDependencySettings.Instance.CompositeFileHandlerPath } + }); + + ClientDependencySettings.Instance.MvcRendererCollection.Add(renderer); + } } } diff --git a/src/Umbraco.Web/Scheduling/KeepAlive.cs b/src/Umbraco.Web/Scheduling/KeepAlive.cs index 5221d8b499..74c9f79d42 100644 --- a/src/Umbraco.Web/Scheduling/KeepAlive.cs +++ b/src/Umbraco.Web/Scheduling/KeepAlive.cs @@ -4,6 +4,7 @@ using System.Threading; using System.Threading.Tasks; using Umbraco.Core; using Umbraco.Core.Logging; +using Umbraco.Core.Sync; namespace Umbraco.Web.Scheduling { @@ -24,6 +25,17 @@ namespace Umbraco.Web.Scheduling public override async Task PerformRunAsync(CancellationToken token) { + // not on slaves nor unknown role servers + switch (_runtime.ServerRole) + { + case ServerRole.Slave: + _logger.Debug("Does not run on slave servers."); + return true; // role may change! + case ServerRole.Unknown: + _logger.Debug("Does not run on servers with unknown role."); + return true; // role may change! + } + // ensure we do not run if not main domain, but do NOT lock it if (_runtime.IsMainDom == false) { diff --git a/src/Umbraco.Web/Scheduling/LatchedBackgroundTaskBase.cs b/src/Umbraco.Web/Scheduling/LatchedBackgroundTaskBase.cs index 11db8da4c8..1c86b873bb 100644 --- a/src/Umbraco.Web/Scheduling/LatchedBackgroundTaskBase.cs +++ b/src/Umbraco.Web/Scheduling/LatchedBackgroundTaskBase.cs @@ -5,7 +5,7 @@ using Umbraco.Core; namespace Umbraco.Web.Scheduling { - public abstract class LatchedBackgroundTaskBase : DisposableObject, ILatchedBackgroundTask + public abstract class LatchedBackgroundTaskBase : DisposableObjectSlim, ILatchedBackgroundTask { private TaskCompletionSource _latch; diff --git a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs index 582e540499..5b0ca2ee1b 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs @@ -1,6 +1,4 @@ using System; -using System.Threading; -using System.Threading.Tasks; using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Core.Publishing; @@ -24,7 +22,7 @@ namespace Umbraco.Web.Scheduling _logger = logger; } - public override async Task PerformRunAsync(CancellationToken token) + public override bool PerformRun() { if (Suspendable.ScheduledPublishing.CanRun == false) return true; // repeat, later @@ -46,15 +44,21 @@ namespace Umbraco.Web.Scheduling return false; // do NOT repeat, going down } + // do NOT run publishing if not properly running + if (_runtime.Level != RuntimeLevel.Run) + { + _logger.Debug("Does not run if run level is not Run."); + return true; // repeat/wait + } + try { - // DO not run publishing if content is re-loading - if (_runtime.Level != RuntimeLevel.Run) - { - var publisher = new ScheduledPublisher(_contentService, _logger); - var count = publisher.CheckPendingAndProcess(); - _logger.Warn("No url for service (yet), skip."); - } + // run + // fixme context & events during scheduled publishing? + // in v7 we create an UmbracoContext and an HttpContext, and cache instructions + // are batched, and we have to explicitely flush them, how is it going to work here? + var publisher = new ScheduledPublisher(_contentService, _logger); + var count = publisher.CheckPendingAndProcess(); } catch (Exception e) { @@ -64,6 +68,6 @@ namespace Umbraco.Web.Scheduling return true; // repeat } - public override bool IsAsync => true; + public override bool IsAsync => false; } } diff --git a/src/Umbraco.Web/Scheduling/ScheduledTasks.cs b/src/Umbraco.Web/Scheduling/ScheduledTasks.cs index 86d8efc9dd..dc0f77e0fc 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledTasks.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledTasks.cs @@ -67,6 +67,9 @@ namespace Umbraco.Web.Scheduling { using (var wc = new HttpClient()) { + // url could be relative, so better set a base url for the http client + wc.BaseAddress = _runtime.ApplicationUrl; + var request = new HttpRequestMessage(HttpMethod.Get, url); //TODO: pass custom the authorization header, currently these aren't really secured! diff --git a/src/Umbraco.Web/Scheduling/SchedulerComponent.cs b/src/Umbraco.Web/Scheduling/SchedulerComponent.cs index 50ba87968b..3cadb1d43e 100644 --- a/src/Umbraco.Web/Scheduling/SchedulerComponent.cs +++ b/src/Umbraco.Web/Scheduling/SchedulerComponent.cs @@ -66,15 +66,16 @@ namespace Umbraco.Web.Scheduling _healthCheckRunner = new BackgroundTaskRunner("HealthCheckNotifier", logger); // we will start the whole process when a successful request is made - UmbracoModule.RouteAttempt += UmbracoModuleRouteAttempt; + UmbracoModule.RouteAttempt += RegisterBackgroundTasksOnce; } - private void UmbracoModuleRouteAttempt(object sender, RoutableAttemptEventArgs e) + private void RegisterBackgroundTasksOnce(object sender, RoutableAttemptEventArgs e) { switch (e.Outcome) { case EnsureRoutableOutcome.IsRoutable: case EnsureRoutableOutcome.NotDocumentRequest: + UmbracoModule.RouteAttempt -= RegisterBackgroundTasksOnce; RegisterBackgroundTasks(); break; } @@ -82,9 +83,6 @@ namespace Umbraco.Web.Scheduling private void RegisterBackgroundTasks() { - // remove handler, we're done - UmbracoModule.RouteAttempt -= UmbracoModuleRouteAttempt; - LazyInitializer.EnsureInitialized(ref _tasks, ref _started, ref _locker, () => { _logger.Debug(() => "Initializing the scheduler"); diff --git a/src/Umbraco.Web/Tour/BackOfficeTourFilter.cs b/src/Umbraco.Web/Tour/BackOfficeTourFilter.cs new file mode 100644 index 0000000000..bcca2e6673 --- /dev/null +++ b/src/Umbraco.Web/Tour/BackOfficeTourFilter.cs @@ -0,0 +1,63 @@ +using System.Text.RegularExpressions; + +namespace Umbraco.Web.Tour +{ + /// + /// Represents a back-office tour filter. + /// + public class BackOfficeTourFilter + { + /// + /// Initializes a new instance of the class. + /// + /// Value to filter out tours by a plugin, can be null + /// Value to filter out a tour file, can be null + /// Value to filter out a tour alias, can be null + /// + /// Depending on what is null will depend on how the filter is applied. + /// If pluginName is not NULL and it's matched then we check if tourFileName is not NULL and it's matched then we check tour alias is not NULL and then match it, + /// if any steps is NULL then the filters upstream are applied. + /// Example, pluginName = "hello", tourFileName="stuff", tourAlias=NULL = we will filter out the tour file "stuff" from the plugin "hello" but not from other plugins if the same file name exists. + /// Example, tourAlias="test.*" = we will filter out all tour aliases that start with the word "test" regardless of the plugin or file name + /// + public BackOfficeTourFilter(Regex pluginName, Regex tourFileName, Regex tourAlias) + { + PluginName = pluginName; + TourFileName = tourFileName; + TourAlias = tourAlias; + } + + /// + /// Gets the plugin name filtering regex. + /// + public Regex PluginName { get; } + + /// + /// Gets the tour filename filtering regex. + /// + public Regex TourFileName { get; } + + /// + /// Gets the tour alias filtering regex. + /// + public Regex TourAlias { get; } + + /// + /// Creates a filter to filter on the plugin name. + /// + public static BackOfficeTourFilter FilterPlugin(Regex pluginName) + => new BackOfficeTourFilter(pluginName, null, null); + + /// + /// Creates a filter to filter on the tour filename. + /// + public static BackOfficeTourFilter FilterFile(Regex tourFileName) + => new BackOfficeTourFilter(null, tourFileName, null); + + /// + /// Creates a filter to filter on the tour alias. + /// + public static BackOfficeTourFilter FilterAlias(Regex tourAlias) + => new BackOfficeTourFilter(null, null, tourAlias); + } +} diff --git a/src/Umbraco.Web/Tour/TourFilterCollection.cs b/src/Umbraco.Web/Tour/TourFilterCollection.cs new file mode 100644 index 0000000000..e221c17170 --- /dev/null +++ b/src/Umbraco.Web/Tour/TourFilterCollection.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using Umbraco.Core.Composing; + +namespace Umbraco.Web.Tour +{ + /// + /// Represents a collection of items. + /// + public class TourFilterCollection : BuilderCollectionBase + { + /// + /// Initializes a new instance of the class. + /// + public TourFilterCollection(IEnumerable items) + : base(items) + { } + } +} diff --git a/src/Umbraco.Web/Tour/TourFilterCollectionBuilder.cs b/src/Umbraco.Web/Tour/TourFilterCollectionBuilder.cs new file mode 100644 index 0000000000..5084975bbd --- /dev/null +++ b/src/Umbraco.Web/Tour/TourFilterCollectionBuilder.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using LightInject; +using Umbraco.Core; +using Umbraco.Core.Composing; + +namespace Umbraco.Web.Tour +{ + /// + /// Builds a collection of items. + /// + public class TourFilterCollectionBuilder : CollectionBuilderBase + { + private readonly HashSet _instances = new HashSet(); + + /// + /// Initializes a new instance of the class. + /// + public TourFilterCollectionBuilder(IServiceContainer container) + : base(container) + { } + + /// + protected override IEnumerable CreateItems(params object[] args) + { + return base.CreateItems(args).Concat(_instances); + } + + /// + /// Adds a filter instance. + /// + public void AddFilter(BackOfficeTourFilter filter) + { + _instances.Add(filter); + } + + /// + /// Removes a filter instance. + /// + public void RemoveFilter(BackOfficeTourFilter filter) + { + _instances.Remove(filter); + } + + /// + /// Removes all filter instances. + /// + public void RemoveAllFilters() + { + _instances.Clear(); + } + + /// + /// Removes filters matching a condition. + /// + public void RemoveFilter(Func predicate) + { + _instances.RemoveWhere(new Predicate(predicate)); + } + + /// + /// Creates and adds a filter instance filtering by plugin name. + /// + public void AddFilterByPlugin(string pluginName) + { + pluginName = pluginName.EnsureStartsWith("^").EnsureEndsWith("$"); + _instances.Add(BackOfficeTourFilter.FilterPlugin(new Regex(pluginName, RegexOptions.IgnoreCase))); + } + + /// + /// Creates and adds a filter instance filtering by tour filename. + /// + public void AddFilterByFile(string filename) + { + filename = filename.EnsureStartsWith("^").EnsureEndsWith("$"); + _instances.Add(BackOfficeTourFilter.FilterFile(new Regex(filename, RegexOptions.IgnoreCase))); + } + } +} diff --git a/src/Umbraco.Web/Trees/ApplicationTreeController.cs b/src/Umbraco.Web/Trees/ApplicationTreeController.cs index 0ab6a4ec75..a37bd08c8b 100644 --- a/src/Umbraco.Web/Trees/ApplicationTreeController.cs +++ b/src/Umbraco.Web/Trees/ApplicationTreeController.cs @@ -82,10 +82,19 @@ namespace Umbraco.Web.Trees private async Task GetRootForMultipleAppTree(ApplicationTree configTree, FormDataCollection queryStrings) { if (configTree == null) throw new ArgumentNullException(nameof(configTree)); - var byControllerAttempt = await configTree.TryGetRootNodeFromControllerTree(queryStrings, ControllerContext); - if (byControllerAttempt.Success) + try { - return byControllerAttempt.Result; + var byControllerAttempt = await configTree.TryGetRootNodeFromControllerTree(queryStrings, ControllerContext); + if (byControllerAttempt.Success) + { + return byControllerAttempt.Result; + } + } + catch (HttpResponseException) + { + //if this occurs its because the user isn't authorized to view that tree, in this case since we are loading multiple trees we + //will just return null so that it's not added to the list. + return null; } var legacyAttempt = configTree.TryGetRootNodeFromLegacyTree(queryStrings, Url, configTree.ApplicationAlias); diff --git a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs index a4e62551fe..dd27c80382 100644 --- a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs @@ -32,12 +32,18 @@ namespace Umbraco.Web.Trees public TreeNode GetTreeNode(string id, FormDataCollection queryStrings) { int asInt; + Guid asGuid = Guid.Empty; if (int.TryParse(id, out asInt) == false) { - throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + if (Guid.TryParse(id, out asGuid) == false) + { + throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + } } - var entity = Services.EntityService.Get(asInt, UmbracoObjectType); + var entity = asGuid == Guid.Empty + ? Services.EntityService.Get(asInt, UmbracoObjectType) + : Services.EntityService.Get(asGuid, UmbracoObjectType); if (entity == null) { throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); @@ -112,102 +118,92 @@ namespace Umbraco.Web.Trees { var nodes = new TreeNodeCollection(); - var altStartId = string.Empty; - if (queryStrings.HasKey(TreeQueryStringParameters.StartNodeId)) - altStartId = queryStrings.GetValue(TreeQueryStringParameters.StartNodeId); var rootIdString = Constants.System.Root.ToString(CultureInfo.InvariantCulture); + var hasAccessToRoot = UserStartNodes.Contains(Constants.System.Root); - //check if a request has been made to render from a specific start node - if (string.IsNullOrEmpty(altStartId) == false && altStartId != "undefined" && altStartId != rootIdString) + var startNodeId = queryStrings.HasKey(TreeQueryStringParameters.StartNodeId) + ? queryStrings.GetValue(TreeQueryStringParameters.StartNodeId) + : string.Empty; + + if (string.IsNullOrEmpty(startNodeId) == false && startNodeId != "undefined" && startNodeId != rootIdString) { - id = altStartId; + // request has been made to render from a specific, non-root, start node + id = startNodeId; - //we need to verify that the user has access to view this node, otherwise we'll render an empty tree collection + // ensure that the user has access to that node, otherwise return the empty tree nodes collection // TODO: in the future we could return a validation statement so we can have some UI to notify the user they don't have access if (HasPathAccess(id, queryStrings) == false) { - Logger.Warn("The user " + Security.CurrentUser.Username + " does not have access to the tree node " + id); - return new TreeNodeCollection(); + Logger.Warn("User " + Security.CurrentUser.Username + " does not have access to node with id " + id); + return nodes; } - // So there's an alt id specified, it's not the root node and the user has access to it, great! But there's one thing we - // need to consider: - // If the tree is being rendered in a dialog view we want to render only the children of the specified id, but - // when the tree is being rendered normally in a section and the current user's start node is not -1, then - // we want to include their start node in the tree as well. - // Therefore, in the latter case, we want to change the id to -1 since we want to render the current user's root node - // and the GetChildEntities method will take care of rendering the correct root node. - // If it is in dialog mode, then we don't need to change anything and the children will just render as per normal. - if (IsDialog(queryStrings) == false && UserStartNodes.Contains(Constants.System.Root) == false) - { - id = Constants.System.Root.ToString(CultureInfo.InvariantCulture); - } + // if the tree is rendered... + // - in a dialog: render only the children of the specific start node, nothing to do + // - in a section: if the current user's start nodes do not contain the root node, we need + // to include these start nodes in the tree too, to provide some context - i.e. change + // start node back to root node, and then GetChildEntities method will take care of the rest. + if (IsDialog(queryStrings) == false && hasAccessToRoot == false) + id = rootIdString; } + // get child entities - if id is root, but user's start nodes do not contain the + // root node, this returns the start nodes instead of root's children var entities = GetChildEntities(id).ToList(); + nodes.AddRange(entities.Select(x => GetSingleTreeNodeWithAccessCheck(x, id, queryStrings)).Where(x => x != null)); - //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) + // if the user does not have access to the root node, what we have is the start nodes, + // but to provide some context we also need to add their topmost nodes when they are not + // topmost nodes themselves (level > 1). + if (id == rootIdString && hasAccessToRoot == false) { - var siteNodeIds = new List(); - //put into array since we might modify the list - foreach (var e in entities.ToArray()) + var topNodeIds = entities.Where(x => x.Level > 1).Select(GetTopNodeId).Where(x => x != 0).Distinct().ToArray(); + if (topNodeIds.Length > 0) { - 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 topNodes = Services.EntityService.GetAll(UmbracoObjectType, topNodeIds.ToArray()); + nodes.AddRange(topNodes.Select(x => GetSingleTreeNodeWithAccessCheck(x, id, queryStrings)).Where(x => x != null)); } - 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; } + private static readonly char[] Comma = { ',' }; + + private int GetTopNodeId(IUmbracoEntity entity) + { + int id; + var parts = entity.Path.Split(Comma, StringSplitOptions.RemoveEmptyEntries); + return parts.Length >= 2 && int.TryParse(parts[1], out id) ? id : 0; + } + protected abstract MenuItemCollection PerformGetMenuForNode(string id, FormDataCollection queryStrings); protected abstract UmbracoObjectTypes UmbracoObjectType { get; } protected IEnumerable GetChildEntities(string id) { - // use helper method to ensure we support both integer and guid lookups - - if (int.TryParse(id, out int iid) == false) + // try to parse id as an integer else use GetEntityFromId + // which will grok Guids, Udis, etc and let use obtain the id + if (int.TryParse(id, out var entityId) == false) { - var idEntity = GetEntityFromId(id); - if (idEntity == null) + var entity = GetEntityFromId(id); + if (entity == null) throw new HttpResponseException(HttpStatusCode.NotFound); - iid = idEntity.Id; + entityId = entity.Id; } // if a request is made for the root node but user has no access to // root node, return start nodes instead - if (iid == Constants.System.Root && UserStartNodes.Contains(Constants.System.Root) == false) + if (entityId == Constants.System.Root && UserStartNodes.Contains(Constants.System.Root) == false) { return UserStartNodes.Length > 0 ? Services.EntityService.GetAll(UmbracoObjectType, UserStartNodes) : Enumerable.Empty(); } - return Services.EntityService.GetChildren(iid, UmbracoObjectType).ToArray(); + return Services.EntityService.GetChildren(entityId, UmbracoObjectType).ToArray(); } /// diff --git a/src/Umbraco.Web/Trees/LegacyTreeDataConverter.cs b/src/Umbraco.Web/Trees/LegacyTreeDataConverter.cs index 4ab67224df..c4710ea12d 100644 --- a/src/Umbraco.Web/Trees/LegacyTreeDataConverter.cs +++ b/src/Umbraco.Web/Trees/LegacyTreeDataConverter.cs @@ -141,7 +141,7 @@ namespace Umbraco.Web.Trees else { // if that doesn't work, try to get the legacy confirm view - var attempt2 = GetLegacyConfirmView(currentAction, currentSection); + var attempt2 = GetLegacyConfirmView(currentAction); if (attempt2) { var view = attempt2.Result; @@ -177,9 +177,8 @@ namespace Umbraco.Web.Trees /// This will look at the legacy IAction's JsFunctionName and convert it to a confirmation dialog view if possible /// /// - /// /// - internal static Attempt GetLegacyConfirmView(IAction action, string currentSection) + internal static Attempt GetLegacyConfirmView(IAction action) { if (action.JsFunctionName.IsNullOrWhiteSpace()) { diff --git a/src/Umbraco.Web/Trees/MacroTreeController.cs b/src/Umbraco.Web/Trees/MacroTreeController.cs new file mode 100644 index 0000000000..5e0a8fa25a --- /dev/null +++ b/src/Umbraco.Web/Trees/MacroTreeController.cs @@ -0,0 +1,73 @@ +using System; +using System.Net.Http.Formatting; +using umbraco; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Web.Models.Trees; +using Umbraco.Web.Mvc; +using System.Linq; +using umbraco.BusinessLogic.Actions; +using Umbraco.Web.WebApi.Filters; +using Umbraco.Core.Services; +using Constants = Umbraco.Core.Constants; + +namespace Umbraco.Web.Trees +{ + [UmbracoTreeAuthorize(Constants.Trees.Macros)] + [Tree(Constants.Applications.Developer, Constants.Trees.Macros, null, sortOrder: 2)] + [LegacyBaseTree(typeof(loadMacros))] + [PluginController("UmbracoTrees")] + [CoreTree] + public class MacroTreeController : TreeController + { + protected override TreeNodeCollection GetTreeNodes(string id, FormDataCollection queryStrings) + { + var intId = id.TryConvertTo(); + if (intId == false) throw new InvalidOperationException("Id must be an integer"); + + var nodes = new TreeNodeCollection(); + + nodes.AddRange( + Services.MacroService.GetAll() + .OrderBy(entity => entity.Name) + .Select(macro => + { + var node = CreateTreeNode(macro.Id.ToInvariantString(), id, queryStrings, macro.Name, "icon-settings-alt", false); + node.Path = "-1," + macro.Id; + node.AssignLegacyJsCallback("javascript:UmbClientMgr.contentFrame('developer/macros/editMacro.aspx?macroID=" + macro.Id + "');"); + return node; + })); + + return nodes; + } + + protected override MenuItemCollection GetMenuForNode(string id, FormDataCollection queryStrings) + { + var menu = new MenuItemCollection(); + + if (id == Constants.System.Root.ToInvariantString()) + { + //set the default to create + menu.DefaultMenuAlias = ActionNew.Instance.Alias; + + // root actions + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias))) + .ConvertLegacyMenuItem(null, Constants.Trees.Macros, queryStrings.GetValue("application")); + + menu.Items.Add(ui.Text("actions", ActionRefresh.Instance.Alias), true); + return menu; + } + + //TODO: This is all hacky ... don't have time to convert the tree, views and dialogs over properly so we'll keep using the legacy dialogs + var menuItem = menu.Items.Add(ActionDelete.Instance, Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias))); + var legacyConfirmView = LegacyTreeDataConverter.GetLegacyConfirmView(ActionDelete.Instance); + if (legacyConfirmView == false) + throw new InvalidOperationException("Could not resolve the confirmation view for the legacy action " + ActionDelete.Instance.Alias); + menuItem.LaunchDialogView( + legacyConfirmView.Result, + Services.TextService.Localize(string.Format("general/{0}", ActionDelete.Instance.Alias))); + + return menu; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Trees/MemberTreeController.cs b/src/Umbraco.Web/Trees/MemberTreeController.cs index cdf9cf8085..a0dbcee233 100644 --- a/src/Umbraco.Web/Trees/MemberTreeController.cs +++ b/src/Umbraco.Web/Trees/MemberTreeController.cs @@ -17,6 +17,7 @@ using Umbraco.Web._Legacy.Actions; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Search; using Constants = Umbraco.Core.Constants; +using Umbraco.Core.Services; namespace Umbraco.Web.Trees { @@ -177,6 +178,17 @@ namespace Umbraco.Web.Trees //add delete option for all members menu.Items.Add(Services.TextService.Localize("actions", ActionDelete.Instance.Alias)); + if (Security.CurrentUser.HasAccessToSensitiveData()) + { + menu.Items.Add(new ExportMember + { + Name = Services.TextService.Localize("actions/export"), + Icon = "download-alt", + Alias = "export" + }); + } + + return menu; } diff --git a/src/Umbraco.Web/Trees/UserTreeController.cs b/src/Umbraco.Web/Trees/UserTreeController.cs index 3f7a6f8ce7..df50d49555 100644 --- a/src/Umbraco.Web/Trees/UserTreeController.cs +++ b/src/Umbraco.Web/Trees/UserTreeController.cs @@ -1,4 +1,7 @@ using System.Net.Http.Formatting; +using umbraco.BusinessLogic.Actions; +using Umbraco.Core; +using Umbraco.Core.Services; using Umbraco.Web.Models.Trees; using Umbraco.Web.Mvc; using Umbraco.Web.WebApi.Filters; @@ -7,7 +10,7 @@ using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.Trees { [UmbracoTreeAuthorize(Constants.Trees.Users)] - [Tree(Constants.Applications.Users, Constants.Trees.Users, "Users", sortOrder: 0)] + [Tree(Constants.Applications.Users, Constants.Trees.Users, null, sortOrder: 0)] [PluginController("UmbracoTrees")] [CoreTree] public class UserTreeController : TreeController @@ -39,6 +42,31 @@ namespace Umbraco.Web.Trees protected override MenuItemCollection GetMenuForNode(string id, FormDataCollection queryStrings) { var menu = new MenuItemCollection(); + + if (id == Constants.System.Root.ToInvariantString()) + { + //Create User + var createMenuItem = menu.Items.CreateMenuItem(Services.TextService.Localize("actions/create")); + createMenuItem.Icon = "add"; + createMenuItem.NavigateToRoute("users/users/overview?subview=users&create=true"); + menu.Items.Add(createMenuItem); + + //This is the same setting used in the global JS for 'showUserInvite' + if (EmailSender.CanSendRequiredEmail) + { + //Invite User (Action import closest type of action to an invite user) + var inviteMenuItem = menu.Items.CreateMenuItem(Services.TextService.Localize("user/invite")); + inviteMenuItem.Icon = "message-unopened"; + inviteMenuItem.NavigateToRoute("users/users/overview?subview=users&invite=true"); + + menu.Items.Add(inviteMenuItem); + } + + return menu; + } + + //There is no context menu options for editing a specific user + //Also we no longer list each user in the tree & in theory never hit this return menu; } } diff --git a/src/Umbraco.Web/Trees/XsltTreeController.cs b/src/Umbraco.Web/Trees/XsltTreeController.cs index 185ec4a192..9afb2f33dc 100644 --- a/src/Umbraco.Web/Trees/XsltTreeController.cs +++ b/src/Umbraco.Web/Trees/XsltTreeController.cs @@ -1,15 +1,69 @@ using System.Collections.Generic; using System.IO; +using System; +using System.Net.Http.Formatting; using Umbraco.Core; +using umbraco; +using umbraco.BusinessLogic.Actions; using Umbraco.Core.IO; using Umbraco.Web.Composing; +using Umbraco.Core.Services; using Umbraco.Web.Models.Trees; +using Umbraco.Web.Mvc; +using Umbraco.Web.WebApi.Filters; +using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.Trees { + [UmbracoTreeAuthorize(Constants.Trees.Xslt)] [Tree(Constants.Applications.Settings, Constants.Trees.Xslt, "XSLT Files", "icon-folder", "icon-folder", sortOrder: 2)] + [PluginController("UmbracoTrees")] + [CoreTree] public class XsltTreeController : FileSystemTreeController { + protected override void OnRenderFileNode(ref TreeNode treeNode) + { + ////TODO: This is all hacky ... don't have time to convert the tree, views and dialogs over properly so we'll keep using the legacy views + treeNode.AssignLegacyJsCallback("javascript:UmbClientMgr.contentFrame('developer/xslt/editXslt.aspx?file=" + treeNode.Id + "');"); + } + + protected override void OnRenderFolderNode(ref TreeNode treeNode) + { + //TODO: This is all hacky ... don't have time to convert the tree, views and dialogs over properly so we'll keep using the legacy views + treeNode.AssignLegacyJsCallback("javascript:void(0);"); + } + + protected override MenuItemCollection GetMenuForFile(string path, FormDataCollection queryStrings) + { + var menu = new MenuItemCollection(); + + //TODO: This is all hacky ... don't have time to convert the tree, views and dialogs over properly so we'll keep using the legacy dialogs + var menuItem = menu.Items.Add(ActionDelete.Instance, Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias))); + var legacyConfirmView = LegacyTreeDataConverter.GetLegacyConfirmView(ActionDelete.Instance); + if (legacyConfirmView == false) + throw new InvalidOperationException("Could not resolve the confirmation view for the legacy action " + ActionDelete.Instance.Alias); + menuItem.LaunchDialogView( + legacyConfirmView.Result, + Services.TextService.Localize("general/delete")); + + return menu; + } + + protected override MenuItemCollection GetMenuForRootNode(FormDataCollection queryStrings) + { + var menu = new MenuItemCollection(); + + //set the default to create + menu.DefaultMenuAlias = ActionNew.Instance.Alias; + + // root actions + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias))) + .ConvertLegacyMenuItem(null, Constants.Trees.Xslt, queryStrings.GetValue("application")); + + menu.Items.Add(ui.Text("actions", ActionRefresh.Instance.Alias), true); + return menu; + } + protected override IFileSystem FileSystem => Current.FileSystems.XsltFileSystem; // fixme inject private static readonly string[] ExtensionsStatic = { "xslt" }; diff --git a/src/Umbraco.Web/UI/JavaScript/JsInitialize.js b/src/Umbraco.Web/UI/JavaScript/JsInitialize.js index 3be8a58983..961fb8e54e 100644 --- a/src/Umbraco.Web/UI/JavaScript/JsInitialize.js +++ b/src/Umbraco.Web/UI/JavaScript/JsInitialize.js @@ -3,6 +3,8 @@ 'lib/angular/1.1.5/angular.min.js', 'lib/underscore/underscore-min.js', + 'lib/moment/moment-with-locales.js', + 'lib/jquery-ui/jquery-ui.min.js', 'lib/jquery-ui-touch-punch/jquery.ui.touch-punch.js', @@ -14,7 +16,7 @@ 'lib/angular-dynamic-locale/tmhDynamicLocale.min.js', 'lib/ng-file-upload/ng-file-upload.min.js', - 'lib/angular-local-storage/angular-local-storage.min.js', + 'lib/angular-local-storage/angular-local-storage.min.js', //"lib/ace-builds/src-min-noconflict/ace.js", diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 88f9430f2b..e3753ed994 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -63,7 +63,7 @@ - + @@ -145,6 +145,7 @@ + @@ -205,11 +206,13 @@ + + @@ -222,13 +225,24 @@ + + + + + + + + + + + @@ -236,6 +250,7 @@ + @@ -365,8 +380,11 @@ - + + + + @@ -649,7 +667,6 @@ - @@ -731,7 +748,7 @@ - + @@ -921,7 +938,7 @@ - + @@ -1045,7 +1062,7 @@ - + diff --git a/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs b/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs index f2675c21ae..c9a10acfa0 100644 --- a/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs +++ b/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs @@ -53,12 +53,11 @@ namespace Umbraco.Web /// protected virtual void ConfigureMiddleware(IAppBuilder app) { - //Ensure owin is configured for Umbraco back office authentication. If you have any front-end OWIN - // cookie configuration, this must be declared after it. + + // Configure OWIN for authentication. + ConfigureUmbracoAuthentication(app); + app - .UseUmbracoBackOfficeCookieAuthentication(Current.RuntimeState, PipelineStage.Authenticate) - .UseUmbracoBackOfficeExternalCookieAuthentication(Current.RuntimeState, PipelineStage.Authenticate) - .UseUmbracoPreviewAuthentication(Current.RuntimeState, PipelineStage.Authorize) .UseSignalR() .FinalizeMiddlewareConfiguration(); } @@ -75,6 +74,20 @@ namespace Umbraco.Web Core.Security.MembershipProviderExtensions.GetUsersMembershipProvider().AsUmbracoMembershipProvider()); } + /// + /// Configure external/OAuth login providers + /// + /// + protected virtual void ConfigureUmbracoAuthentication(IAppBuilder app) + { + // Ensure owin is configured for Umbraco back office authentication. + // Front-end OWIN cookie configuration must be declared after this code. + app + .UseUmbracoBackOfficeCookieAuthentication(Current.RuntimeState, PipelineStage.Authenticate) + .UseUmbracoBackOfficeExternalCookieAuthentication(Current.RuntimeState, PipelineStage.Authenticate) + .UseUmbracoPreviewAuthentication(Current.RuntimeState, PipelineStage.Authorize); + } + 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 9d5f2a12ae..2386e4abef 100644 --- a/src/Umbraco.Web/UmbracoHelper.cs +++ b/src/Umbraco.Web/UmbracoHelper.cs @@ -436,8 +436,13 @@ namespace Umbraco.Web public IPublishedContent Member(object id) { - var asInt = id.TryConvertTo(); - return asInt ? MembershipHelper.GetById(asInt.Result) : MembershipHelper.GetByProviderKey(id); + if (ConvertIdObjectToInt(id, out var intId)) + return Member(intId); + if (ConvertIdObjectToGuid(id, out var guidId)) + return Member(guidId); + if (ConvertIdObjectToUdi(id, out var udiId)) + return Member(udiId); + return null; } public IPublishedContent Member(int id) @@ -699,7 +704,7 @@ namespace Umbraco.Web #region Media - public IPublishedContent TypedMedia(Udi id) + public IPublishedContent Media(Udi id) { var guidUdi = id as GuidUdi; return guidUdi == null ? null : Media(guidUdi.Guid); @@ -839,7 +844,7 @@ namespace Umbraco.Web #region Search /// - /// Searches content + /// Searches content. /// /// /// @@ -851,7 +856,36 @@ namespace Umbraco.Web } /// - /// Searhes content + /// Searches content. + /// + /// + /// + /// + /// + /// + /// + /// + public IEnumerable TypedSearch(int skip, int take, out int totalRecords, string term, bool useWildCards = true, string searchProvider = null) + { + return ContentQuery.Search(skip, take, out totalRecords, term, useWildCards, searchProvider); + } + + /// + /// Searhes content. + /// + /// + /// + /// + /// + /// + /// + public IEnumerable TypedSearch(int skip, int take, out int totalRecords, Examine.SearchCriteria.ISearchCriteria criteria, Examine.Providers.BaseSearchProvider searchProvider = null) + { + return ContentQuery.Search(skip, take, out totalRecords, criteria, searchProvider); + } + + /// + /// Searhes content. /// /// /// diff --git a/src/Umbraco.Web/UriUtility.cs b/src/Umbraco.Web/UriUtility.cs index d5ad7d3854..0a76262f6c 100644 --- a/src/Umbraco.Web/UriUtility.cs +++ b/src/Umbraco.Web/UriUtility.cs @@ -68,7 +68,7 @@ namespace Umbraco.Web if (!GlobalSettings.UseDirectoryUrls) path += ".aspx"; else if (UmbracoConfig.For.UmbracoSettings().RequestHandler.AddTrailingSlash) - path += "/"; + path = path.EnsureEndsWith("/"); } path = ToAbsolute(path); diff --git a/src/Umbraco.Web/WebApi/Binders/MemberBinder.cs b/src/Umbraco.Web/WebApi/Binders/MemberBinder.cs index a1471ab640..2bb3c795f0 100644 --- a/src/Umbraco.Web/WebApi/Binders/MemberBinder.cs +++ b/src/Umbraco.Web/WebApi/Binders/MemberBinder.cs @@ -14,6 +14,7 @@ using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.WebApi.Filters; using System.Linq; +using System.Net.Http; using Umbraco.Core.Models.Membership; using Umbraco.Core.Services.Implement; using Umbraco.Web; @@ -85,17 +86,8 @@ namespace Umbraco.Web.WebApi.Binders if (member == null) { throw new InvalidOperationException("Could not find member with key " + key); - } + } - var standardProps = Constants.Conventions.Member.GetStandardPropertyTypeStubs(); - - //remove all membership properties, these values are set with the membership provider. - var exclude = standardProps.Select(x => x.Value.Alias).ToArray(); - - foreach (var remove in exclude) - { - member.Properties.Remove(remove); - } return member; } @@ -232,6 +224,14 @@ namespace Umbraco.Web.WebApi.Binders return base.ValidatePropertyData(postedItem, actionContext); } + /// + /// This ensures that the internal membership property types are removed from validation before processing the validation + /// since those properties are actually mapped to real properties of the IMember. + /// This also validates any posted data for fields that are sensitive. + /// + /// + /// + /// protected override bool ValidateProperties(ContentItemBasic postedItem, HttpActionContext actionContext) { var propertiesToValidate = postedItem.Properties.ToList(); @@ -242,9 +242,41 @@ namespace Umbraco.Web.WebApi.Binders propertiesToValidate.RemoveAll(property => property.Alias == remove); } - return ValidateProperties(propertiesToValidate.ToArray(), postedItem.PersistedContent.Properties.ToArray(), actionContext); - } + var httpCtx = actionContext.Request.TryGetHttpContext(); + if (httpCtx.Success == false) + { + actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.NotFound, "No http context"); + return false; + } + var umbCtx = httpCtx.Result.GetUmbracoContext(); + //if the user doesn't have access to sensitive values, then we need to validate the incoming properties to check + //if a sensitive value is being submitted. + if (umbCtx.Security.CurrentUser.HasAccessToSensitiveData() == false) + { + var sensitiveProperties = postedItem.PersistedContent.ContentType + .PropertyTypes.Where(x => postedItem.PersistedContent.ContentType.IsSensitiveProperty(x.Alias)) + .ToList(); + + foreach (var sensitiveProperty in sensitiveProperties) + { + var prop = propertiesToValidate.FirstOrDefault(x => x.Alias == sensitiveProperty.Alias); + + if (prop != null) + { + //this should not happen, this means that there was data posted for a sensitive property that + //the user doesn't have access to, which means that someone is trying to hack the values. + + var message = string.Format("property with alias: {0} cannot be posted", prop.Alias); + actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.NotFound, new InvalidOperationException(message)); + return false; + } + } + } + + return ValidateProperties(propertiesToValidate, postedItem.PersistedContent.Properties.ToList(), actionContext); + } + internal bool ValidateUniqueLogin(MemberSave contentItem, MembershipProvider membershipProvider, HttpActionContext actionContext) { if (contentItem == null) throw new ArgumentNullException("contentItem"); diff --git a/src/Umbraco.Web/WebApi/Filters/CheckIfUserTicketDataIsStaleAttribute.cs b/src/Umbraco.Web/WebApi/Filters/CheckIfUserTicketDataIsStaleAttribute.cs index d2781dbd2d..411c393824 100644 --- a/src/Umbraco.Web/WebApi/Filters/CheckIfUserTicketDataIsStaleAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/CheckIfUserTicketDataIsStaleAttribute.cs @@ -20,6 +20,9 @@ namespace Umbraco.Web.WebApi.Filters /// to what is persisted for the current user and will update the current auth ticket with the correct data if required and output /// a custom response header for the UI to be notified of it. /// + /// + /// This could/should be created as a filter on the BackOfficeCookieAuthenticationProvider just like the SecurityStampValidator does + /// public sealed class CheckIfUserTicketDataIsStaleAttribute : ActionFilterAttribute { public override async Task OnActionExecutingAsync(HttpActionContext actionContext, CancellationToken cancellationToken) @@ -107,12 +110,12 @@ namespace Umbraco.Web.WebApi.Filters if (owinCtx) { var signInManager = owinCtx.Result.GetBackOfficeSignInManager(); - - //ensure the remainder of the request has the correct principal set - actionContext.Request.SetPrincipalForRequest(user); - + var backOfficeIdentityUser = Mapper.Map(user); await signInManager.SignInAsync(backOfficeIdentityUser, isPersistent: true, rememberBrowser: false); + + //ensure the remainder of the request has the correct principal set + actionContext.Request.SetPrincipalForRequest(owinCtx.Result.Request.User); //flag that we've made changes actionContext.Request.Properties[typeof(CheckIfUserTicketDataIsStaleAttribute).Name] = true; diff --git a/src/Umbraco.Web/WebApi/Filters/ContentItemValidationHelper.cs b/src/Umbraco.Web/WebApi/Filters/ContentItemValidationHelper.cs index 02396fc92f..fcf1413c32 100644 --- a/src/Umbraco.Web/WebApi/Filters/ContentItemValidationHelper.cs +++ b/src/Umbraco.Web/WebApi/Filters/ContentItemValidationHelper.cs @@ -72,7 +72,7 @@ namespace Umbraco.Web.WebApi.Filters /// protected virtual bool ValidateProperties(ContentItemBasic postedItem, HttpActionContext actionContext) { - return ValidateProperties(postedItem.Properties.ToArray(), postedItem.PersistedContent.Properties.ToArray(), actionContext); + return ValidateProperties(postedItem.Properties.ToList(), postedItem.PersistedContent.Properties.ToList(), actionContext); } /// @@ -82,7 +82,7 @@ namespace Umbraco.Web.WebApi.Filters /// /// /// - protected bool ValidateProperties(ContentPropertyBasic[] postedProperties , Property[] persistedProperties, HttpActionContext actionContext) + protected bool ValidateProperties(List postedProperties , List persistedProperties, HttpActionContext actionContext) { foreach (var p in postedProperties) { @@ -124,8 +124,12 @@ namespace Umbraco.Web.WebApi.Filters continue; } - // get the posted value - var postedValue = postedItem.Properties.Single(x => x.Alias == p.Alias).Value; + //get the posted value for this property, this may be null in cases where the property was marked as readonly which means + //the angular app will not post that value. + var postedProp = postedItem.Properties.FirstOrDefault(x => x.Alias == p.Alias); + if (postedProp == null) continue; + + var postedValue = postedProp.Value; // validate var valueEditor = editor.GetValueEditor(p.DataType.Configuration); diff --git a/src/Umbraco.Web/WebApi/Filters/FeatureAuthorizeAttribute.cs b/src/Umbraco.Web/WebApi/Filters/FeatureAuthorizeAttribute.cs new file mode 100644 index 0000000000..dd1954663f --- /dev/null +++ b/src/Umbraco.Web/WebApi/Filters/FeatureAuthorizeAttribute.cs @@ -0,0 +1,23 @@ +using System.Web.Http; +using System.Web.Http.Controllers; +using Umbraco.Web.Features; + +namespace Umbraco.Web.WebApi.Filters +{ + /// + /// Ensures that the controller is an authorized feature. + /// + /// Else returns unauthorized. + public sealed class FeatureAuthorizeAttribute : AuthorizeAttribute + { + protected override bool IsAuthorized(HttpActionContext actionContext) + { + //if no features resolver has been set then return true, this will occur in unit tests and we don't want users to have to set a resolver + //just so their unit tests work. + if (FeaturesResolver.HasCurrent == false) return true; + + var controllerType = actionContext.ControllerContext.ControllerDescriptor.ControllerType; + return FeaturesResolver.Current.Features.IsEnabled(controllerType); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingContentAttribute.cs b/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingContentAttribute.cs index 59dd55304e..74001040e3 100644 --- a/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingContentAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingContentAttribute.cs @@ -97,30 +97,19 @@ namespace Umbraco.Web.WebApi.Filters ids.Add(((dynamic)items[i]).Id); } //get all the permissions for these nodes in one call - var permissions = _userService.GetPermissions(user, ids.ToArray()).ToArray(); + var permissions = _userService.GetPermissions(user, ids.ToArray()); var toRemove = new List(); foreach (dynamic item in items) - { - var nodePermission = permissions.Where(x => x.EntityId == Convert.ToInt32(item.Id)).ToArray(); - //if there are no permissions for this id then we need to check what the user's default - // permissions are. - if (nodePermission.Length == 0) + { + //get the combined permission set across all user groups for this node + //we're in the world of dynamics here so we need to cast + var nodePermission = ((IEnumerable)permissions.GetAllPermissions(item.Id)).ToArray(); + + //if the permission being checked doesn't exist then remove the item + if (nodePermission.Contains(_permissionToCheck.ToString(CultureInfo.InvariantCulture)) == false) { - //var defaultP = user.DefaultPermissions - toRemove.Add(item); - } - else - { - foreach (var n in nodePermission) - { - //if the permission being checked doesn't exist then remove the item - if (n.AssignedPermissions.Contains(_permissionToCheck.ToString(CultureInfo.InvariantCulture)) == false) - { - toRemove.Add(item); - } - } - } + } } foreach (var item in toRemove) { diff --git a/src/Umbraco.Web/WebApi/UmbracoApiControllerBase.cs b/src/Umbraco.Web/WebApi/UmbracoApiControllerBase.cs index 99b3e0825e..cb6e0a73f6 100644 --- a/src/Umbraco.Web/WebApi/UmbracoApiControllerBase.cs +++ b/src/Umbraco.Web/WebApi/UmbracoApiControllerBase.cs @@ -9,6 +9,7 @@ using Umbraco.Core.Logging; using Umbraco.Core.Persistence; using Umbraco.Core.Services; using Umbraco.Web.Security; +using Umbraco.Web.WebApi.Filters; namespace Umbraco.Web.WebApi { @@ -16,6 +17,7 @@ namespace Umbraco.Web.WebApi /// Provides a base class for Umbraco API controllers. /// /// These controllers are NOT auto-routed. + [FeatureAuthorize] public abstract class UmbracoApiControllerBase : ApiController { private UmbracoHelper _umbracoHelper; diff --git a/src/Umbraco.Web/WebServices/ExamineManagementApiController.cs b/src/Umbraco.Web/WebServices/ExamineManagementApiController.cs index ab07429223..69e88f2c7b 100644 --- a/src/Umbraco.Web/WebServices/ExamineManagementApiController.cs +++ b/src/Umbraco.Web/WebServices/ExamineManagementApiController.cs @@ -8,7 +8,9 @@ using Examine; using Examine.LuceneEngine; using Examine.LuceneEngine.Providers; using Examine.Providers; +using Lucene.Net.Index; using Lucene.Net.Search; +using Lucene.Net.Store; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Logging; @@ -194,21 +196,42 @@ namespace Umbraco.Web.WebServices try { indexer.RebuildIndex(); - } + } + // fixme - Examine issues + // cannot enable that piece of code from v7 as Index.Write.Unlock does not seem to exist anymore? + //catch (LockObtainFailedException) + //{ + // //this will occur if the index is locked (which it should defo not be!) so in this case we'll forcibly unlock it and try again + + // try + // { + // IndexWriter.Unlock(indexer.GetLuceneDirectory()); + // indexer.RebuildIndex(); + // } + // catch (Exception e) + // { + // return HandleException(e, indexer); + // } + //} catch (Exception ex) { - //ensure it's not listening - indexer.IndexOperationComplete -= Indexer_IndexOperationComplete; - Logger.Error("An error occurred rebuilding index", ex); - var response = Request.CreateResponse(HttpStatusCode.Conflict); - response.Content = new StringContent(string.Format("The index could not be rebuilt at this time, most likely there is another thread currently writing to the index. Error: {0}", ex)); - response.ReasonPhrase = "Could Not Rebuild"; - return response; + return HandleException(ex, indexer); } } return msg; } + private HttpResponseMessage HandleException(Exception ex, LuceneIndexer indexer) + { + //ensure it's not listening + indexer.IndexOperationComplete -= Indexer_IndexOperationComplete; + Logger.Error("An error occurred rebuilding index", ex); + var response = Request.CreateResponse(HttpStatusCode.Conflict); + response.Content = new StringContent(string.Format("The index could not be rebuilt at this time, most likely there is another thread currently writing to the index. Error: {0}", ex)); + response.ReasonPhrase = "Could Not Rebuild"; + return response; + } + //static listener so it's not GC'd private static void Indexer_IndexOperationComplete(object sender, EventArgs e) { diff --git a/src/Umbraco.Web/_Legacy/UI/LegacyDialogHandler.cs b/src/Umbraco.Web/_Legacy/UI/LegacyDialogHandler.cs index 07ed6c03d7..cfa93dc0f6 100644 --- a/src/Umbraco.Web/_Legacy/UI/LegacyDialogHandler.cs +++ b/src/Umbraco.Web/_Legacy/UI/LegacyDialogHandler.cs @@ -120,8 +120,11 @@ namespace Umbraco.Web._Legacy.UI internal static bool UserHasCreateAccess(HttpContextBase httpContext, IUser umbracoUser, string nodeType) { var task = GetTaskForOperation(httpContext, umbracoUser, Operation.Create, nodeType); + + //if no task was found it will use the default task and we cannot validate the application assigned so return true if (task == null) - throw new InvalidOperationException($"Could not task for operation {Operation.Create} for node type {nodeType}."); + return true; + return task is LegacyDialogTask ltask ? ltask.ValidateUserForApplication() : true; } @@ -141,8 +144,11 @@ namespace Umbraco.Web._Legacy.UI internal static bool UserHasDeleteAccess(HttpContextBase httpContext, User umbracoUser, string nodeType) { var task = GetTaskForOperation(httpContext, umbracoUser, Operation.Delete, nodeType); + + //if no task was found it will use the default task and we cannot validate the application assigned so return true if (task == null) - throw new InvalidOperationException($"Could not task for operation {Operation.Delete} for node type {nodeType}"); + return true; + return task is LegacyDialogTask ltask ? ltask.ValidateUserForApplication() : true; }