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.dllTrue
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
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;
}