diff --git a/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs b/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs
index a5a2a4eb60..748e90469d 100644
--- a/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs
+++ b/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs
@@ -63,9 +63,9 @@ namespace Umbraco.Web.Cache
// fixme - not sure I like these?
TagsValueConverter.ClearCaches();
- MultipleMediaPickerPropertyConverter.ClearCaches();
+ MediaPickerLegacyValueConverter.ClearCaches();
SliderValueConverter.ClearCaches();
- MediaPickerPropertyConverter.ClearCaches();
+ MediaPickerValueConverter.ClearCaches();
// notify
_facadeService.Notify(payloads);
diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs
index 057c77cf04..4777cad6da 100644
--- a/src/Umbraco.Web/Editors/AuthenticationController.cs
+++ b/src/Umbraco.Web/Editors/AuthenticationController.cs
@@ -50,6 +50,60 @@ namespace Umbraco.Web.Editors
get { return _signInManager ?? (_signInManager = TryGetOwinContext().Result.GetBackOfficeSignInManager()); }
}
+ ///
+ /// Returns the configuration for the backoffice user membership provider - used to configure the change password dialog
+ ///
+ ///
+ [WebApi.UmbracoAuthorize(requireApproval: false)]
+ public IDictionary GetMembershipProviderConfig()
+ {
+ //TODO: Check if the current PasswordValidator is an IMembershipProviderPasswordValidator, if
+ //it's not than we should return some generic defaults
+ var provider = Core.Security.MembershipProviderExtensions.GetUsersMembershipProvider();
+ return provider.GetConfiguration(Services.UserService);
+ }
+
+ ///
+ /// Checks if a valid token is specified for an invited user and if so logs the user in and returns the user object
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// This will also update the security stamp for the user so it can only be used once
+ ///
+ [ValidateAngularAntiForgeryToken]
+ public async Task PostVerifyInvite([FromUri]int id, [FromUri]string token)
+ {
+ if (string.IsNullOrWhiteSpace(token))
+ throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound));
+
+ var decoded = token.FromUrlBase64();
+ if (decoded.IsNullOrWhiteSpace())
+ throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound));
+
+ var identityUser = await UserManager.FindByIdAsync(id);
+ if (identityUser == null)
+ throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound));
+
+ var result = await UserManager.ConfirmEmailAsync(id, decoded);
+
+ if (result.Succeeded == false)
+ {
+ throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse(string.Join(", ", result.Errors)));
+ }
+
+ Request.TryGetOwinContext().Result.Authentication.SignOut(
+ Core.Constants.Security.BackOfficeAuthenticationType,
+ Core.Constants.Security.BackOfficeExternalAuthenticationType);
+
+ await SignInManager.SignInAsync(identityUser, false, false);
+
+ var user = ApplicationContext.Services.UserService.GetUserById(id);
+
+ return Mapper.Map(user);
+ }
+
[WebApi.UmbracoAuthorize]
[ValidateAngularAntiForgeryToken]
public async Task PostUnLinkLogin(UnLinkLoginModel unlinkLoginModel)
@@ -97,9 +151,10 @@ namespace Umbraco.Web.Editors
///
[WebApi.UmbracoAuthorize]
[SetAngularAntiForgeryTokens]
+ [CheckIfUserTicketDataIsStale]
public UserDetail GetCurrentUser()
{
- var user = Services.UserService.GetUserById(UmbracoContext.Security.GetUserId());
+ var user = UmbracoContext.Security.CurrentUser;
var result = Mapper.Map(user);
var httpContextAttempt = TryGetHttpContext();
if (httpContextAttempt.Success)
@@ -111,6 +166,38 @@ namespace Umbraco.Web.Editors
return result;
}
+ ///
+ /// When a user is invited they are not approved but we need to resolve the partially logged on (non approved)
+ /// user.
+ ///
+ ///
+ ///
+ /// We cannot user GetCurrentUser since that requires they are approved, this is the same as GetCurrentUser but doesn't require them to be approved
+ ///
+ [WebApi.UmbracoAuthorize(requireApproval:false)]
+ [SetAngularAntiForgeryTokens]
+ public UserDetail GetCurrentInvitedUser()
+ {
+ var user = UmbracoContext.Security.CurrentUser;
+
+ if (user.IsApproved)
+ {
+ //if they are approved, than they are no longer invited and we can return an error
+ throw new HttpResponseException(Request.CreateUserNoAccessResponse());
+ }
+
+ var result = Mapper.Map(user);
+ var httpContextAttempt = TryGetHttpContext();
+ if (httpContextAttempt.Success)
+ {
+ //set their remaining seconds
+ result.SecondsUntilTimeout = httpContextAttempt.Result.GetRemainingAuthSeconds();
+ }
+
+ return result;
+ }
+
+ //TODO: This should be on the CurrentUserController?
[WebApi.UmbracoAuthorize]
[ValidateAngularAntiForgeryToken]
public async Task> GetCurrentUserLinkedLogins()
@@ -128,6 +215,8 @@ namespace Umbraco.Web.Editors
{
var http = EnsureHttpContext();
+ //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
var result = await SignInManager.PasswordSignInAsync(
loginModel.Username, loginModel.Password, isPersistent: true, shouldLockout: true);
@@ -136,7 +225,7 @@ namespace Umbraco.Web.Editors
case SignInStatus.Success:
//get the user
- var user = Security.GetBackOfficeUser(loginModel.Username);
+ var user = Services.UserService.GetByUsername(loginModel.Username);
return SetPrincipalAndReturnUserDetail(user);
case SignInStatus.RequiresVerification:
@@ -162,7 +251,7 @@ namespace Umbraco.Web.Editors
typeof(IUmbracoBackOfficeTwoFactorOptions) + ".GetTwoFactorView returned an empty string"));
}
- var attemptedUser = Security.GetBackOfficeUser(loginModel.Username);
+ var attemptedUser = Services.UserService.GetByUsername(loginModel.Username);
//create a with information to display a custom two factor send code view
var verifyResponse = Request.CreateResponse(HttpStatusCode.PaymentRequired, new
@@ -281,7 +370,7 @@ namespace Umbraco.Web.Editors
{
case SignInStatus.Success:
//get the user
- var user = Security.GetBackOfficeUser(userName);
+ var user = Services.UserService.GetByUsername(userName);
return SetPrincipalAndReturnUserDetail(user);
case SignInStatus.LockedOut:
return Request.CreateValidationErrorResponse("User is locked out");
@@ -290,7 +379,7 @@ namespace Umbraco.Web.Editors
return Request.CreateValidationErrorResponse("Invalid code");
}
}
-
+
///
/// Processes a set password request. Validates the request and sets a new password.
///
diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs
index 8f1ff94a16..99f9838e7e 100644
--- a/src/Umbraco.Web/Editors/BackOfficeController.cs
+++ b/src/Umbraco.Web/Editors/BackOfficeController.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Configuration;
using System.Globalization;
using System.IO;
using System.Linq;
@@ -8,10 +7,8 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
-using System.Web.Configuration;
using System.Web.Mvc;
using System.Web.UI;
-using ClientDependency.Core.Config;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin.Security;
@@ -20,24 +17,19 @@ using Newtonsoft.Json.Linq;
using Umbraco.Core;
using Umbraco.Core.Cache;
using Umbraco.Core.Configuration;
-using Umbraco.Core.Configuration.UmbracoSettings;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
using Umbraco.Core.Manifest;
using Umbraco.Core.Models.Identity;
+using Umbraco.Core.Models.Membership;
using Umbraco.Core.Security;
-using Umbraco.Web.HealthCheck;
using Umbraco.Web.Models;
-using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Mvc;
-using Umbraco.Web.PropertyEditors;
using Umbraco.Web.Security.Identity;
using Umbraco.Web.Trees;
using Umbraco.Web.UI.JavaScript;
-using Umbraco.Web.WebServices;
using Umbraco.Core.Services;
using Umbraco.Web.Composing;
-using Umbraco.Web.Security;
using Action = Umbraco.Web._Legacy.Actions.Action;
using Constants = Umbraco.Core.Constants;
@@ -85,6 +77,66 @@ namespace Umbraco.Web.Editors
() => View(GlobalSettings.Path.EnsureEndsWith('/') + "Views/Default.cshtml", new BackOfficeModel { Path = GlobalSettings.Path }));
}
+ [HttpGet]
+ public async Task VerifyInvite(string invite)
+ {
+ if (invite == null)
+ {
+ Logger.Warn("VerifyUser endpoint reached with invalid token: NULL");
+ return RedirectToAction("Default");
+ }
+
+ var parts = Server.UrlDecode(invite).Split('|');
+
+ if (parts.Length != 2)
+ {
+ Logger.Warn("VerifyUser endpoint reached with invalid token: " + invite);
+ return RedirectToAction("Default");
+ }
+
+ var token = parts[1];
+
+ var decoded = token.FromUrlBase64();
+ if (decoded.IsNullOrWhiteSpace())
+ {
+ Logger.Warn("VerifyUser endpoint reached with invalid token: " + invite);
+ return RedirectToAction("Default");
+ }
+
+ var id = parts[0];
+ int intId;
+ if (int.TryParse(id, out intId) == false)
+ {
+ Logger.Warn("VerifyUser endpoint reached with invalid token: " + invite);
+ return RedirectToAction("Default");
+ }
+
+ var identityUser = await UserManager.FindByIdAsync(intId);
+ if (identityUser == null)
+ {
+ Logger.Warn("VerifyUser endpoint reached with non existing user: " + id);
+ return RedirectToAction("Default");
+ }
+
+ var result = await UserManager.ConfirmEmailAsync(intId, decoded);
+
+ if (result.Succeeded == false)
+ {
+ Logger.Warn("Could not verify email, Error: " + string.Join(",", result.Errors) + ", Token: " + invite);
+ return RedirectToAction("Default");
+ }
+
+ //sign the user in
+
+ AuthenticationManager.SignOut(
+ Core.Constants.Security.BackOfficeAuthenticationType,
+ Core.Constants.Security.BackOfficeExternalAuthenticationType);
+
+ await SignInManager.SignInAsync(identityUser, false, false);
+
+ return new RedirectResult(Url.Action("Default") + "#/login/false?invite=1");
+ }
+
///
/// This Action is used by the installer when an upgrade is detected but the admin user is not logged in. We need to
/// ensure the user is authenticated before the install takes place so we redirect here to show the standard login screen.
@@ -189,12 +241,7 @@ namespace Umbraco.Web.Editors
return new JsonNetResult { Data = gridConfig.EditorsConfig.Editors, Formatting = Formatting.Indented };
}
- private string GetMaxRequestLength()
- {
- var section = ConfigurationManager.GetSection("system.web/httpRuntime") as HttpRuntimeSection;
- if (section == null) return string.Empty;
- return section.MaxRequestLength.ToString();
- }
+
///
/// Returns the JavaScript object representing the static server variables javascript object
@@ -204,263 +251,21 @@ namespace Umbraco.Web.Editors
[MinifyJavaScriptResult(Order = 1)]
public JavaScriptResult ServerVariables()
{
- Func getResult = () =>
- {
- var defaultVals = new Dictionary
- {
- {
- "umbracoUrls", new Dictionary
- {
- //TODO: Add 'umbracoApiControllerBaseUrl' which people can use in JS
- // to prepend their URL. We could then also use this in our own resources instead of
- // having each url defined here explicitly - we can do that in v8! for now
- // for umbraco services we'll stick to explicitly defining the endpoints.
-
- {"externalLoginsUrl", Url.Action("ExternalLogin", "BackOffice")},
- {"externalLinkLoginsUrl", Url.Action("LinkLogin", "BackOffice")},
- {"legacyTreeJs", Url.Action("LegacyTreeJs", "BackOffice")},
- {"manifestAssetList", Url.Action("GetManifestAssetList", "BackOffice")},
- {"gridConfig", Url.Action("GetGridConfig", "BackOffice")},
- {"serverVarsJs", Url.Action("Application", "BackOffice")},
- //API URLs
- {
- "packagesRestApiBaseUrl", UmbracoConfig.For.UmbracoSettings().PackageRepositories.GetDefault().RestApiUrl
- },
- {
- "redirectUrlManagementApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl(
- controller => controller.GetEnableState())
- },
- {
- "embedApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl(
- controller => controller.GetEmbed("", 0, 0))
- },
- {
- "userApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl(
- controller => controller.PostDisableUser(0))
- },
- {
- "contentApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl(
- controller => controller.PostSave(null))
- },
- {
- "mediaApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl(
- controller => controller.GetRootMedia())
- },
- {
- "imagesApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl(
- controller => controller.GetBigThumbnail(0))
- },
- {
- "sectionApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl(
- controller => controller.GetSections())
- },
- {
- "treeApplicationApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl(
- controller => controller.GetApplicationTrees(null, null, null, true))
- },
- {
- "contentTypeApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl(
- controller => controller.GetAllowedChildren(0))
- },
- {
- "mediaTypeApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl(
- controller => controller.GetAllowedChildren(0))
- },
- {
- "macroApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl(
- controller => controller.GetMacroParameters(0))
- },
- {
- "authenticationApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl(
- controller => controller.PostLogin(null))
- },
- {
- "currentUserApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl(
- controller => controller.GetMembershipProviderConfig())
- },
- {
- "legacyApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl(
- controller => controller.DeleteLegacyItem(null, null, null))
- },
- {
- "entityApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl(
- controller => controller.GetById(0, UmbracoEntityTypes.Media))
- },
- {
- "dataTypeApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl(
- controller => controller.GetById(0))
- },
- {
- "dashboardApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl(
- controller => controller.GetDashboard(null))
- },
- {
- "logApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl(
- controller => controller.GetEntityLog(0))
- },
- {
- "gravatarApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl(
- controller => controller.GetCurrentUserGravatarUrl())
- },
- {
- "memberApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl(
- controller => controller.GetByKey(Guid.Empty))
- },
- {
- "packageInstallApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl(
- controller => controller.Fetch(string.Empty))
- },
- {
- "relationApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl(
- controller => controller.GetById(0))
- },
- {
- "rteApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl(
- controller => controller.GetConfiguration())
- },
- {
- "stylesheetApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl(
- controller => controller.GetAll())
- },
- {
- "memberTypeApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl(
- controller => controller.GetAllTypes())
- },
- {
- "memberGroupApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl(
- controller => controller.GetAllGroups())
- },
- {
- "updateCheckApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl(
- controller => controller.GetCheck())
- },
- {
- "tagApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl(
- controller => controller.GetAllTags(null))
- },
- {
- "templateApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl(
- controller => controller.GetById(0))
- },
- {
- "memberTreeBaseUrl", Url.GetUmbracoApiServiceBaseUrl(
- controller => controller.GetNodes("-1", null))
- },
- {
- "mediaTreeBaseUrl", Url.GetUmbracoApiServiceBaseUrl(
- controller => controller.GetNodes("-1", null))
- },
- {
- "contentTreeBaseUrl", Url.GetUmbracoApiServiceBaseUrl(
- controller => controller.GetNodes("-1", null))
- },
- {
- "tagsDataBaseUrl", Url.GetUmbracoApiServiceBaseUrl(
- controller => controller.GetTags(""))
- },
- {
- "examineMgmtBaseUrl", Url.GetUmbracoApiServiceBaseUrl(
- controller => controller.GetIndexerDetails())
- },
- {
- "xmlDataIntegrityBaseUrl", Url.GetUmbracoApiServiceBaseUrl(
- controller => controller.CheckContentXmlTable())
- },
- {
- "healthCheckBaseUrl", Url.GetUmbracoApiServiceBaseUrl(
- controller => controller.GetAllHealthChecks())
- },
- {
- "templateQueryApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl(
- controller => controller.PostTemplateQuery(null))
- },
- {
- "codeFileApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl(
- controller => controller.GetByPath("", ""))
- },
- {
- "facadeStatusBaseUrl", Url.GetUmbracoApiServiceBaseUrl(
- controller => controller.GetFacadeStatusUrl())
- },
- {
- "nuCacheStatusBaseUrl", Url.GetUmbracoApiServiceBaseUrl(
- controller => controller.GetStatus())
- }
- }
- },
- {
- "umbracoSettings", new Dictionary
- {
- {"umbracoPath", GlobalSettings.Path},
- {"mediaPath", IOHelper.ResolveUrl(SystemDirectories.Media).TrimEnd('/')},
- {"appPluginsPath", IOHelper.ResolveUrl(SystemDirectories.AppPlugins).TrimEnd('/')},
- {
- "imageFileTypes",
- string.Join(",", UmbracoConfig.For.UmbracoSettings().Content.ImageFileTypes)
- },
- {
- "disallowedUploadFiles",
- string.Join(",", UmbracoConfig.For.UmbracoSettings().Content.DisallowedUploadFiles)
- },
- {
- "allowedUploadFiles",
- string.Join(",", UmbracoConfig.For.UmbracoSettings().Content.AllowedUploadFiles)
- },
- {
- "maxFileSize",
- GetMaxRequestLength()
- },
- {"keepUserLoggedIn", UmbracoConfig.For.UmbracoSettings().Security.KeepUserLoggedIn},
- {"cssPath", IOHelper.ResolveUrl(SystemDirectories.Css).TrimEnd('/')},
- {"allowPasswordReset", UmbracoConfig.For.UmbracoSettings().Security.AllowPasswordReset},
- {"loginBackgroundImage", UmbracoConfig.For.UmbracoSettings().Content.LoginBackgroundImage},
- }
- },
- {
- "umbracoPlugins", new Dictionary
- {
- {"trees", GetTreePluginsMetaData()}
- }
- },
- {
- "isDebuggingEnabled", HttpContext.IsDebuggingEnabled
- },
- {
- "application", GetApplicationState()
- },
- {
- "externalLogins", new Dictionary
- {
- {
- "providers", HttpContext.GetOwinContext().Authentication.GetExternalAuthenticationTypes()
- .Where(p => p.Properties.ContainsKey("UmbracoBackOffice"))
- .Select(p => new
- {
- authType = p.AuthenticationType, caption = p.Caption,
- //TODO: Need to see if this exposes any sensitive data!
- properties = p.Properties
- })
- .ToArray()
- }
- }
- }
- };
-
- //Parse the variables to a string
- return ServerVariablesParser.Parse(defaultVals);
- };
+ var serverVars = new BackOfficeServerVariables(Url, ApplicationContext, UmbracoConfig.For.UmbracoSettings());
//cache the result if debugging is disabled
var result = HttpContext.IsDebuggingEnabled
- ? getResult()
+ ? ServerVariablesParser.Parse(serverVars.GetServerVariables())
: ApplicationCache.RuntimeCache.GetCacheItem(
typeof(BackOfficeController) + "ServerVariables",
- () => getResult(),
+ () => ServerVariablesParser.Parse(serverVars.GetServerVariables()),
new TimeSpan(0, 10, 0));
return JavaScript(result);
}
+
+
[HttpPost]
public ActionResult ExternalLogin(string provider, string redirectUrl = null)
{
@@ -642,60 +447,52 @@ namespace Umbraco.Web.Editors
}
else
{
- var defaultUserType = autoLinkOptions.GetDefaultUserType(UmbracoContext, loginInfo);
- var userType = Services.UserService.GetUserTypeByAlias(defaultUserType);
- if (userType == null)
+ 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[] { "Could not auto-link this account, the specified User Type does not exist: " + defaultUserType };
+ 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
{
-
- 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 autoLinkUser = new BackOfficeIdentityUser()
+ var linkResult = await UserManager.AddLoginAsync(autoLinkUser.Id, loginInfo.Login);
+ if (linkResult.Succeeded == false)
{
- Email = loginInfo.Email,
- Name = loginInfo.ExternalIdentity.Name,
- UserTypeAlias = userType.Alias,
- AllowedSections = autoLinkOptions.GetDefaultAllowedSections(UmbracoContext, loginInfo),
- Culture = autoLinkOptions.GetDefaultCulture(UmbracoContext, loginInfo),
- UserName = loginInfo.Email
- };
+ ViewData[TokenExternalSignInError] = linkResult.Errors;
- //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;
+ //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
{
- 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);
}
}
}
@@ -708,59 +505,6 @@ namespace Umbraco.Web.Editors
return false;
}
- ///
- /// Returns the server variables regarding the application state
- ///
- ///
- private Dictionary GetApplicationState()
- {
- if (_runtime.Level != RuntimeLevel.Run)
- return null;
-
- return new Dictionary
- {
- // assembly version
- { "assemblyVersion", UmbracoVersion.AssemblyVersion },
- // Umbraco version
- { "version", _runtime.SemanticVersion.ToSemanticString() },
- // client dependency version,
- { "cdf", ClientDependencySettings.Instance.Version },
- // for dealing with virtual paths on the client side when hosted in virtual directories
- { "applicationPath", _runtime.ApplicationVirtualPath.EnsureEndsWith('/') },
- // server's GMT time offset in minutes
- { "serverTimeOffset", Convert.ToInt32(DateTimeOffset.Now.Offset.TotalMinutes) }
- };
- }
-
-
- private IEnumerable> GetTreePluginsMetaData()
- {
- var treeTypes = Current.TypeLoader.GetAttributedTreeControllers(); // fixme inject
- //get all plugin trees with their attributes
- var treesWithAttributes = treeTypes.Select(x => new
- {
- tree = x,
- attributes =
- x.GetCustomAttributes(false)
- }).ToArray();
-
- var pluginTreesWithAttributes = treesWithAttributes
- //don't resolve any tree decorated with CoreTreeAttribute
- .Where(x => x.attributes.All(a => (a is CoreTreeAttribute) == false))
- //we only care about trees with the PluginControllerAttribute
- .Where(x => x.attributes.Any(a => a is PluginControllerAttribute))
- .ToArray();
-
- return (from p in pluginTreesWithAttributes
- let treeAttr = p.attributes.OfType().Single()
- let pluginAttr = p.attributes.OfType().Single()
- select new Dictionary
- {
- {"alias", treeAttr.Alias}, {"packageFolder", pluginAttr.AreaName}
- }).ToArray();
-
- }
-
///
/// Returns the JavaScript blocks for any legacy trees declared
///
diff --git a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs
new file mode 100644
index 0000000000..66aaccd96a
--- /dev/null
+++ b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs
@@ -0,0 +1,409 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Configuration;
+using System.Linq;
+using System.Web;
+using System.Web.Configuration;
+using System.Web.Mvc;
+using ClientDependency.Core.Config;
+using Microsoft.Owin;
+using Microsoft.Owin.Security;
+using Umbraco.Core;
+using Umbraco.Core.Composing;
+using Umbraco.Core.Configuration;
+using Umbraco.Core.Configuration.UmbracoSettings;
+using Umbraco.Core.IO;
+using Umbraco.Web.HealthCheck;
+using Umbraco.Web.Models.ContentEditing;
+using Umbraco.Web.Mvc;
+using Umbraco.Web.PropertyEditors;
+using Umbraco.Web.Trees;
+using Umbraco.Web.WebServices;
+
+namespace Umbraco.Web.Editors
+{
+ ///
+ /// Used to collect the server variables for use in the back office angular app
+ ///
+ internal class BackOfficeServerVariables
+ {
+ private readonly UrlHelper _urlHelper;
+ private readonly IRuntimeState _runtimeState;
+ private readonly HttpContextBase _httpContext;
+ private readonly IOwinContext _owinContext;
+
+ public BackOfficeServerVariables(UrlHelper urlHelper, IRuntimeState runtimeState)
+ {
+ _urlHelper = urlHelper;
+ _runtimeState = runtimeState;
+ _httpContext = _urlHelper.RequestContext.HttpContext;
+ _owinContext = _httpContext.GetOwinContext();
+ }
+
+ ///
+ /// Returns the server variables for non-authenticated users
+ ///
+ ///
+ internal Dictionary BareMinimumServerVariables()
+ {
+ //this is the filter for the keys that we'll keep based on the full version of the server vars
+ var keepOnlyKeys = new Dictionary
+ {
+ {"umbracoUrls", new[] {"authenticationApiBaseUrl", "serverVarsJs", "externalLoginsUrl", "currentUserApiBaseUrl"}},
+ {"umbracoSettings", new[] {"allowPasswordReset", "imageFileTypes", "maxFileSize", "loginBackgroundImage"}},
+ {"application", new[] {"applicationPath", "cacheBuster"}},
+ {"isDebuggingEnabled", new string[] { }}
+ };
+ //now do the filtering...
+ var defaults = GetServerVariables();
+ foreach (var key in defaults.Keys.ToArray())
+ {
+ if (keepOnlyKeys.ContainsKey(key) == false)
+ {
+ defaults.Remove(key);
+ }
+ else
+ {
+ var asDictionary = defaults[key] as IDictionary;
+ if (asDictionary != null)
+ {
+ var toKeep = keepOnlyKeys[key];
+ foreach (var k in asDictionary.Keys.Cast().ToArray())
+ {
+ if (toKeep.Contains(k) == false)
+ {
+ asDictionary.Remove(k);
+ }
+ }
+ }
+ }
+ }
+
+ //TODO: This is ultra confusing! this same key is used for different things, when returning the full app when authenticated it is this URL but when not auth'd it's actually the ServerVariables address
+ // so based on compat and how things are currently working we need to replace the serverVarsJs one
+ ((Dictionary) defaults["umbracoUrls"])["serverVarsJs"] = _urlHelper.Action("ServerVariables", "BackOffice");
+
+ return defaults;
+ }
+
+ ///
+ /// Returns the server variables for authenticated users
+ ///
+ ///
+ internal Dictionary GetServerVariables()
+ {
+ var defaultVals = new Dictionary
+ {
+ {
+ "umbracoUrls", new Dictionary
+ {
+ //TODO: Add 'umbracoApiControllerBaseUrl' which people can use in JS
+ // to prepend their URL. We could then also use this in our own resources instead of
+ // having each url defined here explicitly - we can do that in v8! for now
+ // for umbraco services we'll stick to explicitly defining the endpoints.
+
+ {"externalLoginsUrl", _urlHelper.Action("ExternalLogin", "BackOffice")},
+ {"externalLinkLoginsUrl", _urlHelper.Action("LinkLogin", "BackOffice")},
+ {"legacyTreeJs", _urlHelper.Action("LegacyTreeJs", "BackOffice")},
+ {"manifestAssetList", _urlHelper.Action("GetManifestAssetList", "BackOffice")},
+ {"gridConfig", _urlHelper.Action("GetGridConfig", "BackOffice")},
+ //TODO: This is ultra confusing! this same key is used for different things, when returning the full app when authenticated it is this URL but when not auth'd it's actually the ServerVariables address
+ {"serverVarsJs", _urlHelper.Action("Application", "BackOffice")},
+ //API URLs
+ {
+ "packagesRestApiBaseUrl", UmbracoConfig.For.UmbracoSettings().PackageRepositories.GetDefault().RestApiUrl
+ },
+ {
+ "redirectUrlManagementApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl(
+ controller => controller.GetEnableState())
+ },
+ {
+ "embedApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl(
+ controller => controller.GetEmbed("", 0, 0))
+ },
+ {
+ "userApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl(
+ controller => controller.PostSaveUser(null))
+ },
+ {
+ "userGroupsApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl(
+ controller => controller.PostSaveUserGroup(null))
+ },
+ {
+ "contentApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl(
+ controller => controller.PostSave(null))
+ },
+ {
+ "mediaApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl(
+ controller => controller.GetRootMedia())
+ },
+ {
+ "imagesApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl(
+ controller => controller.GetBigThumbnail(0))
+ },
+ {
+ "sectionApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl(
+ controller => controller.GetSections())
+ },
+ {
+ "treeApplicationApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl(
+ controller => controller.GetApplicationTrees(null, null, null, true))
+ },
+ {
+ "contentTypeApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl(
+ controller => controller.GetAllowedChildren(0))
+ },
+ {
+ "mediaTypeApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl(
+ controller => controller.GetAllowedChildren(0))
+ },
+ {
+ "macroApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl(
+ controller => controller.GetMacroParameters(0))
+ },
+ {
+ "authenticationApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl(
+ controller => controller.PostLogin(null))
+ },
+ {
+ "currentUserApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl(
+ controller => controller.PostChangePassword(null))
+ },
+ {
+ "legacyApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl(
+ controller => controller.DeleteLegacyItem(null, null, null))
+ },
+ {
+ "entityApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl(
+ controller => controller.GetById(0, UmbracoEntityTypes.Media))
+ },
+ {
+ "dataTypeApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl(
+ controller => controller.GetById(0))
+ },
+ {
+ "dashboardApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl(
+ controller => controller.GetDashboard(null))
+ },
+ {
+ "logApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl(
+ controller => controller.GetEntityLog(0))
+ },
+ {
+ "memberApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl(
+ controller => controller.GetByKey(Guid.Empty))
+ },
+ {
+ "packageInstallApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl(
+ controller => controller.Fetch(string.Empty))
+ },
+ {
+ "relationApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl(
+ controller => controller.GetById(0))
+ },
+ {
+ "rteApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl(
+ controller => controller.GetConfiguration())
+ },
+ {
+ "stylesheetApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl(
+ controller => controller.GetAll())
+ },
+ {
+ "memberTypeApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl(
+ controller => controller.GetAllTypes())
+ },
+ {
+ "updateCheckApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl(
+ controller => controller.GetCheck())
+ },
+ {
+ "tagApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl(
+ controller => controller.GetAllTags(null))
+ },
+ {
+ "templateApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl(
+ controller => controller.GetById(0))
+ },
+ {
+ "memberTreeBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl(
+ controller => controller.GetNodes("-1", null))
+ },
+ {
+ "mediaTreeBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl(
+ controller => controller.GetNodes("-1", null))
+ },
+ {
+ "contentTreeBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl(
+ controller => controller.GetNodes("-1", null))
+ },
+ {
+ "tagsDataBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl(
+ controller => controller.GetTags(""))
+ },
+ {
+ "examineMgmtBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl(
+ controller => controller.GetIndexerDetails())
+ },
+ {
+ "xmlDataIntegrityBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl(
+ controller => controller.CheckContentXmlTable())
+ },
+ {
+ "healthCheckBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl(
+ controller => controller.GetAllHealthChecks())
+ },
+ {
+ "templateQueryApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl(
+ controller => controller.PostTemplateQuery(null))
+ },
+ {
+ "codeFileApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl(
+ controller => controller.GetByPath("", ""))
+ },
+ {
+ "facadeStatusBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl(
+ controller => controller.GetFacadeStatusUrl())
+ },
+ {
+ "nuCacheStatusBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl(
+ controller => controller.GetStatus())
+ }
+ }
+ },
+ {
+ "umbracoSettings", new Dictionary
+ {
+ {"umbracoPath", GlobalSettings.Path},
+ {"mediaPath", IOHelper.ResolveUrl(SystemDirectories.Media).TrimEnd('/')},
+ {"appPluginsPath", IOHelper.ResolveUrl(SystemDirectories.AppPlugins).TrimEnd('/')},
+ {
+ "imageFileTypes",
+ string.Join(",", UmbracoConfig.For.UmbracoSettings().Content.ImageFileTypes)
+ },
+ {
+ "disallowedUploadFiles",
+ string.Join(",", UmbracoConfig.For.UmbracoSettings().Content.DisallowedUploadFiles)
+ },
+ {
+ "allowedUploadFiles",
+ string.Join(",", UmbracoConfig.For.UmbracoSettings().Content.AllowedUploadFiles)
+ },
+ {
+ "maxFileSize",
+ GetMaxRequestLength()
+ },
+ {"keepUserLoggedIn", UmbracoConfig.For.UmbracoSettings().Security.KeepUserLoggedIn},
+ {"cssPath", IOHelper.ResolveUrl(SystemDirectories.Css).TrimEnd('/')},
+ {"allowPasswordReset", UmbracoConfig.For.UmbracoSettings().Security.AllowPasswordReset},
+ {"loginBackgroundImage", UmbracoConfig.For.UmbracoSettings().Content.LoginBackgroundImage},
+ {"emailServerConfigured", GlobalSettings.HasSmtpServerConfigured(_httpContext.Request.ApplicationPath)},
+ }
+ },
+ {
+ "umbracoPlugins", new Dictionary
+ {
+ {"trees", GetTreePluginsMetaData()}
+ }
+ },
+ {
+ "isDebuggingEnabled", _httpContext.IsDebuggingEnabled
+ },
+ {
+ "application", GetApplicationState()
+ },
+ {
+ "externalLogins", new Dictionary
+ {
+ {
+ "providers", _owinContext.Authentication.GetExternalAuthenticationTypes()
+ .Where(p => p.Properties.ContainsKey("UmbracoBackOffice"))
+ .Select(p => new
+ {
+ authType = p.AuthenticationType, caption = p.Caption,
+ //TODO: Need to see if this exposes any sensitive data!
+ properties = p.Properties
+ })
+ .ToArray()
+ }
+ }
+ }
+ };
+ return defaultVals;
+ }
+
+ private IEnumerable> GetTreePluginsMetaData()
+ {
+ var treeTypes = TreeControllerTypes.Value;
+ //get all plugin trees with their attributes
+ var treesWithAttributes = treeTypes.Select(x => new
+ {
+ tree = x,
+ attributes =
+ x.GetCustomAttributes(false)
+ }).ToArray();
+
+ var pluginTreesWithAttributes = treesWithAttributes
+ //don't resolve any tree decorated with CoreTreeAttribute
+ .Where(x => x.attributes.All(a => (a is CoreTreeAttribute) == false))
+ //we only care about trees with the PluginControllerAttribute
+ .Where(x => x.attributes.Any(a => a is PluginControllerAttribute))
+ .ToArray();
+
+ return (from p in pluginTreesWithAttributes
+ let treeAttr = p.attributes.OfType().Single()
+ let pluginAttr = p.attributes.OfType().Single()
+ select new Dictionary
+ {
+ {"alias", treeAttr.Alias}, {"packageFolder", pluginAttr.AreaName}
+ }).ToArray();
+
+ }
+
+ ///
+ /// A lazy reference to all tree controller types
+ ///
+ ///
+ /// We are doing this because if we constantly resolve the tree controller types from the PluginManager it will re-scan and also re-log that
+ /// it's resolving which is unecessary and annoying.
+ ///
+ private static readonly Lazy> TreeControllerTypes
+ = new Lazy>(() => Current.TypeLoader.GetAttributedTreeControllers().ToArray()); // fixme inject
+
+ ///
+ /// Returns the server variables regarding the application state
+ ///
+ ///
+ private Dictionary GetApplicationState()
+ {
+ if (_runtimeState.Level != RuntimeLevel.Run)
+ return null;
+
+ var app = new Dictionary
+ {
+ {"assemblyVersion", UmbracoVersion.AssemblyVersion}
+ };
+
+ var version = _runtimeState.SemanticVersion.ToSemanticString();
+
+ app.Add("cacheBuster", $"{version}.{ClientDependencySettings.Instance.Version}".GenerateHash());
+ app.Add("version", version);
+
+ //useful for dealing with virtual paths on the client side when hosted in virtual directories especially
+ app.Add("applicationPath", _httpContext.Request.ApplicationPath.EnsureEndsWith('/'));
+
+ //add the server's GMT time offset in minutes
+ app.Add("serverTimeOffset", Convert.ToInt32(DateTimeOffset.Now.Offset.TotalMinutes));
+
+ return app;
+ }
+
+ private static string GetMaxRequestLength()
+ {
+ return ConfigurationManager.GetSection("system.web/httpRuntime") is HttpRuntimeSection section
+ ? section.MaxRequestLength.ToString()
+ : string.Empty;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs
index b8d03985d1..a7adf07049 100644
--- a/src/Umbraco.Web/Editors/ContentController.cs
+++ b/src/Umbraco.Web/Editors/ContentController.cs
@@ -9,6 +9,7 @@ using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.ModelBinding;
using AutoMapper;
+using umbraco.BusinessLogic.Actions;
using Umbraco.Core;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
@@ -23,6 +24,7 @@ using Umbraco.Web.WebApi.Binders;
using Umbraco.Web.WebApi.Filters;
using Umbraco.Core.Persistence.Querying;
using Umbraco.Web.PublishedCache;
+using Umbraco.Core.Events;
using Constants = Umbraco.Core.Constants;
namespace Umbraco.Web.Editors
@@ -71,6 +73,120 @@ namespace Umbraco.Web.Editors
return foundContent.Select(Mapper.Map);
}
+ ///
+ /// Updates the permissions for a content item for a particular user group
+ ///
+ ///
+ ///
+ ///
+ /// Permission check is done for letter 'R' which is for which the user must have access to to update
+ ///
+ [EnsureUserPermissionForContent("saveModel.ContentId", 'R')]
+ public IEnumerable PostSaveUserGroupPermissions(UserGroupPermissionsSave saveModel)
+ {
+ if (saveModel.ContentId <= 0) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound));
+
+ var content = Services.ContentService.GetById(saveModel.ContentId);
+ if (content == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound));
+
+ //current permissions explicitly assigned to this content item
+ var contentPermissions = Services.ContentService.GetPermissionsForEntity(content)
+ .ToDictionary(x => x.UserGroupId, x => x);
+
+ var allUserGroups = Services.UserService.GetAllUserGroups().ToArray();
+
+ //loop through each user group
+ foreach (var userGroup in allUserGroups)
+ {
+ //check if there's a permission set posted up for this user group
+ IEnumerable groupPermissions;
+ if (saveModel.AssignedPermissions.TryGetValue(userGroup.Id, out groupPermissions))
+ {
+ //create a string collection of the assigned letters
+ var groupPermissionCodes = groupPermissions.ToArray();
+
+ //check if there are no permissions assigned for this group save model, if that is the case we want to reset the permissions
+ //for this group/node which will go back to the defaults
+ if (groupPermissionCodes.Length == 0)
+ {
+ Services.UserService.RemoveUserGroupPermissions(userGroup.Id, content.Id);
+ }
+ //check if they are the defaults, if so we should just remove them if they exist since it's more overhead having them stored
+ else if (userGroup.Permissions.UnsortedSequenceEqual(groupPermissionCodes))
+ {
+ //only remove them if they are actually currently assigned
+ if (contentPermissions.ContainsKey(userGroup.Id))
+ {
+ //remove these permissions from this node for this group since the ones being assigned are the same as the defaults
+ Services.UserService.RemoveUserGroupPermissions(userGroup.Id, content.Id);
+ }
+ }
+ //if they are different we need to update, otherwise there's nothing to update
+ else if (contentPermissions.ContainsKey(userGroup.Id) == false || contentPermissions[userGroup.Id].AssignedPermissions.UnsortedSequenceEqual(groupPermissionCodes) == false)
+ {
+
+ Services.UserService.ReplaceUserGroupPermissions(userGroup.Id, groupPermissionCodes.Select(x => x[0]), content.Id);
+ }
+ }
+ }
+
+ return GetDetailedPermissions(content, allUserGroups);
+ }
+
+ ///
+ /// Returns the user group permissions for user groups assigned to this node
+ ///
+ ///
+ ///
+ ///
+ /// Permission check is done for letter 'R' which is for which the user must have access to to view
+ ///
+ [EnsureUserPermissionForContent("contentId", 'R')]
+ public IEnumerable GetDetailedPermissions(int contentId)
+ {
+ if (contentId <= 0) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound));
+ var content = Services.ContentService.GetById(contentId);
+ if (content == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound));
+
+ var allUserGroups = Services.UserService.GetAllUserGroups();
+
+ return GetDetailedPermissions(content, allUserGroups);
+ }
+
+ private IEnumerable GetDetailedPermissions(IContent content, IEnumerable allUserGroups)
+ {
+ //get all user groups and map their default permissions to the AssignedUserGroupPermissions model.
+ //we do this because not all groups will have true assigned permissions for this node so if they don't have assigned permissions, we need to show the defaults.
+
+ var defaultPermissionsByGroup = Mapper.Map>(allUserGroups).ToArray();
+
+ var defaultPermissionsAsDictionary = defaultPermissionsByGroup
+ .ToDictionary(x => Convert.ToInt32(x.Id), x => x);
+
+ //get the actual assigned permissions
+ var assignedPermissionsByGroup = Services.ContentService.GetPermissionsForEntity(content).ToArray();
+
+ //iterate over assigned and update the defaults with the real values
+ foreach (var assignedGroupPermission in assignedPermissionsByGroup)
+ {
+ var defaultUserGroupPermissions = defaultPermissionsAsDictionary[assignedGroupPermission.UserGroupId];
+
+ //clone the default permissions model to the assigned ones
+ defaultUserGroupPermissions.AssignedPermissions = AssignedUserGroupPermissions.ClonePermissions(defaultUserGroupPermissions.DefaultPermissions);
+
+ //since there is custom permissions assigned to this node for this group, we need to clear all of the default permissions
+ //and we'll re-check it if it's one of the explicitly assigned ones
+ foreach (var permission in defaultUserGroupPermissions.AssignedPermissions.SelectMany(x => x.Value))
+ {
+ permission.Checked = false;
+ permission.Checked = assignedGroupPermission.AssignedPermissions.Contains(permission.PermissionCode, StringComparer.InvariantCulture);
+ }
+
+ }
+
+ return defaultPermissionsByGroup;
+ }
+
///
/// Returns an item to be used to display the recycle bin for content
///
@@ -94,6 +210,36 @@ namespace Umbraco.Web.Editors
return display;
}
+ public ContentItemDisplay GetBlueprintById(int id)
+ {
+ var foundContent = Services.ContentService.GetBlueprintById(id);
+ if (foundContent == null)
+ {
+ HandleContentNotFound(id);
+ }
+
+ var content = Mapper.Map(foundContent);
+
+ SetupBlueprint(content, foundContent);
+
+ return content;
+ }
+
+ private static void SetupBlueprint(ContentItemDisplay content, IContent persistedContent)
+ {
+ content.AllowPreview = false;
+
+ //set a custom path since the tree that renders this has the content type id as the parent
+ content.Path = string.Format("-1,{0},{1}", persistedContent.ContentTypeId, content.Id);
+
+ content.AllowedActions = new[] {"A"};
+
+ var excludeProps = new[] {"_umb_urls", "_umb_releasedate", "_umb_expiredate", "_umb_template"};
+ var propsTab = content.Tabs.Last();
+ propsTab.Properties = propsTab.Properties
+ .Where(p => excludeProps.Contains(p.Alias) == false);
+ }
+
///
/// Gets the content json for the content id
///
@@ -153,6 +299,26 @@ namespace Umbraco.Web.Editors
return mapped;
}
+ [OutgoingEditorModelEvent]
+ public ContentItemDisplay GetEmpty(int blueprintId)
+ {
+ var blueprint = Services.ContentService.GetBlueprintById(blueprintId);
+ if (blueprint == null)
+ {
+ throw new HttpResponseException(HttpStatusCode.NotFound);
+ }
+
+ blueprint.Id = 0;
+ blueprint.Name = string.Empty;
+
+ var mapped = Mapper.Map(blueprint);
+
+ //remove this tab if it exists: umbContainerView
+ var containerTab = mapped.Tabs.FirstOrDefault(x => x.Alias == Constants.Conventions.PropertyGroups.ListViewGroupName);
+ mapped.Tabs = mapped.Tabs.Except(new[] { containerTab });
+ return mapped;
+ }
+
///
/// Gets the Url for a given node ID
///
@@ -253,6 +419,7 @@ namespace Umbraco.Web.Editors
///
/// Returns permissions for all nodes passed in for the current user
+ /// TODO: This should be moved to the CurrentUserController?
///
///
///
@@ -264,11 +431,18 @@ namespace Umbraco.Web.Editors
.ToDictionary(x => x.EntityId, x => x.AssignedPermissions);
}
+ ///
+ /// Checks a nodes permission for the current user
+ /// TODO: This should be moved to the CurrentUserController?
+ ///
+ ///
+ ///
+ ///
[HttpGet]
public bool HasPermission(string permissionToCheck, int nodeId)
{
- var p = Services.UserService.GetPermissions(Security.CurrentUser, nodeId).FirstOrDefault();
- if (p != null && p.AssignedPermissions.Contains(permissionToCheck.ToString(CultureInfo.InvariantCulture)))
+ var p = Services.UserService.GetPermissions(Security.CurrentUser, nodeId).GetAllPermissions();
+ if (p.Contains(permissionToCheck.ToString(CultureInfo.InvariantCulture)))
{
return true;
}
@@ -276,6 +450,69 @@ namespace Umbraco.Web.Editors
return false;
}
+ ///
+ /// Creates a blueprint from a content item
+ ///
+ /// The content id to copy
+ /// The name of the blueprint
+ ///
+ [HttpPost]
+ public SimpleNotificationModel CreateBlueprintFromContent([FromUri]int contentId, [FromUri]string name)
+ {
+ if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value cannot be null or whitespace.", "name");
+
+ var content = Services.ContentService.GetById(contentId);
+ if (content == null)
+ throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound));
+
+ EnsureUniqueName(name, content, "name");
+
+ var blueprint = Services.ContentService.CreateContentFromBlueprint(content, name, Security.GetUserId());
+
+ Services.ContentService.SaveBlueprint(blueprint, Security.GetUserId());
+
+ var notificationModel = new SimpleNotificationModel();
+ notificationModel.AddSuccessNotification(
+ Services.TextService.Localize("blueprints/createdBlueprintHeading"),
+ Services.TextService.Localize("blueprints/createdBlueprintMessage", new[]{ content.Name})
+ );
+
+ return notificationModel;
+ }
+
+ private void EnsureUniqueName(string name, IContent content, string modelName)
+ {
+ var existing = Services.ContentService.GetBlueprintsForContentTypes(content.ContentTypeId);
+ if (existing.Any(x => x.Name == name && x.Id != content.Id))
+ {
+ ModelState.AddModelError(modelName, Services.TextService.Localize("blueprints/duplicateBlueprintMessage"));
+ throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState));
+ }
+ }
+
+ ///
+ /// Saves content
+ ///
+ ///
+ [FileUploadCleanupFilter]
+ [ContentPostValidate]
+ public ContentItemDisplay PostSaveBlueprint(
+ [ModelBinder(typeof(ContentItemBinder))] ContentItemSave contentItem)
+ {
+ var contentItemDisplay = PostSaveInternal(contentItem,
+ content =>
+ {
+ EnsureUniqueName(content.Name, content, "Name");
+
+ Services.ContentService.SaveBlueprint(contentItem.PersistedContent, Security.CurrentUser.Id);
+ //we need to reuse the underlying logic so return the result that it wants
+ return Attempt.Succeed(new OperationStatus(OperationStatusType.Success, new EventMessages()));
+ });
+ SetupBlueprint(contentItemDisplay, contentItemDisplay.PersistedContent);
+
+ return contentItemDisplay;
+ }
+
///
/// Saves content
///
@@ -285,6 +522,12 @@ namespace Umbraco.Web.Editors
public ContentItemDisplay PostSave(
[ModelBinder(typeof(ContentItemBinder))]
ContentItemSave contentItem)
+ {
+ return PostSaveInternal(contentItem,
+ content => Services.ContentService.WithResult().Save(contentItem.PersistedContent, Security.CurrentUser.Id));
+ }
+
+ private ContentItemDisplay PostSaveInternal(ContentItemSave contentItem, Func> saveMethod)
{
//If we've reached here it means:
// * Our model has been bound
@@ -292,7 +535,6 @@ namespace Umbraco.Web.Editors
// * any file attachments have been saved to their temporary location for us to use
// * we have a reference to the DTO object and the persisted object
// * Permissions are valid
-
MapPropertyValues(contentItem);
//We need to manually check the validation results here because:
@@ -332,7 +574,7 @@ namespace Umbraco.Web.Editors
if (contentItem.Action == ContentSaveAction.Save || contentItem.Action == ContentSaveAction.SaveNew)
{
//save the item
- var saveResult = Services.ContentService.WithResult().Save(contentItem.PersistedContent, Security.CurrentUser.Id);
+ var saveResult = saveMethod(contentItem.PersistedContent);
wasCancelled = saveResult.Success == false && saveResult.Result.StatusType == OperationStatusType.FailedCancelledByEvent;
}
@@ -362,8 +604,8 @@ namespace Umbraco.Web.Editors
if (wasCancelled == false)
{
display.AddSuccessNotification(
- Services.TextService.Localize("speechBubbles/editContentSavedHeader"),
- Services.TextService.Localize("speechBubbles/editContentSavedText"));
+ Services.TextService.Localize("speechBubbles/editContentSavedHeader"),
+ Services.TextService.Localize("speechBubbles/editContentSavedText"));
}
else
{
@@ -375,8 +617,8 @@ namespace Umbraco.Web.Editors
if (wasCancelled == false)
{
display.AddSuccessNotification(
- Services.TextService.Localize("speechBubbles/editContentSendToPublish"),
- Services.TextService.Localize("speechBubbles/editContentSendToPublishText"));
+ Services.TextService.Localize("speechBubbles/editContentSendToPublish"),
+ Services.TextService.Localize("speechBubbles/editContentSendToPublishText"));
}
else
{
@@ -399,6 +641,8 @@ namespace Umbraco.Web.Editors
throw new HttpResponseException(Request.CreateValidationErrorResponse(display));
}
+ display.PersistedContent = contentItem.PersistedContent;
+
return display;
}
@@ -435,6 +679,22 @@ namespace Umbraco.Web.Editors
}
+ [HttpDelete]
+ [HttpPost]
+ public HttpResponseMessage DeleteBlueprint(int id)
+ {
+ var found = Services.ContentService.GetBlueprintById(id);
+
+ if (found == null)
+ {
+ return HandleContentNotFound(id, false);
+ }
+
+ Services.ContentService.DeleteBlueprint(found);
+
+ return Request.CreateResponse(HttpStatusCode.OK);
+ }
+
///
/// Moves an item to the recycle bin, if it is already there then it will permanently delete it
///
@@ -754,8 +1014,6 @@ namespace Umbraco.Web.Editors
}
}
-
-
///
/// Performs a permissions check for the user to check if it has access to the node based on
/// start node and/or permissions for the node
@@ -764,6 +1022,7 @@ namespace Umbraco.Web.Editors
///
///
///
+ ///
/// The content to lookup, if the contentItem is not specified
///
/// Specifies the already resolved content item to check against
@@ -773,10 +1032,16 @@ namespace Umbraco.Web.Editors
IUser user,
IUserService userService,
IContentService contentService,
+ IEntityService entityService,
int nodeId,
char[] permissionsToCheck = null,
IContent contentItem = null)
{
+ if (storage == null) throw new ArgumentNullException("storage");
+ if (user == null) throw new ArgumentNullException("user");
+ if (userService == null) throw new ArgumentNullException("userService");
+ if (contentService == null) throw new ArgumentNullException("contentService");
+ if (entityService == null) throw new ArgumentNullException("entityService");
if (contentItem == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinContent)
{
@@ -790,35 +1055,33 @@ namespace Umbraco.Web.Editors
{
throw new HttpResponseException(HttpStatusCode.NotFound);
}
-
+
var hasPathAccess = (nodeId == Constants.System.Root)
- ? UserExtensions.HasPathAccess(
- Constants.System.Root.ToInvariantString(),
- user.StartContentId,
- Constants.System.RecycleBinContent)
- : (nodeId == Constants.System.RecycleBinContent)
- ? UserExtensions.HasPathAccess(
- Constants.System.RecycleBinContent.ToInvariantString(),
- user.StartContentId,
- Constants.System.RecycleBinContent)
- : user.HasPathAccess(contentItem);
+ ? user.HasContentRootAccess(entityService)
+ : (nodeId == Constants.System.RecycleBinContent)
+ ? user.HasContentBinAccess(entityService)
+ : user.HasPathAccess(contentItem, entityService);
if (hasPathAccess == false)
{
return false;
}
- if (permissionsToCheck == null || permissionsToCheck.Any() == false)
+ if (permissionsToCheck == null || permissionsToCheck.Length == 0)
{
return true;
}
- var permission = userService.GetPermissions(user, nodeId).FirstOrDefault();
+ //get the implicit/inherited permissions for the user for this path,
+ //if there is no content item for this id, than just use the id as the path (i.e. -1 or -20)
+ var path = contentItem != null ? contentItem.Path : nodeId.ToString();
+ var permission = userService.GetPermissionsForPath(user, path);
var allowed = true;
foreach (var p in permissionsToCheck)
{
- if (permission == null || permission.AssignedPermissions.Contains(p.ToString(CultureInfo.InvariantCulture)) == false)
+ if (permission == null
+ || permission.GetAllPermissions().Contains(p.ToString(CultureInfo.InvariantCulture)) == false)
{
allowed = false;
}
diff --git a/src/Umbraco.Web/Editors/ContentPostValidateAttribute.cs b/src/Umbraco.Web/Editors/ContentPostValidateAttribute.cs
index b1fc989cd2..806836cd53 100644
--- a/src/Umbraco.Web/Editors/ContentPostValidateAttribute.cs
+++ b/src/Umbraco.Web/Editors/ContentPostValidateAttribute.cs
@@ -23,18 +23,21 @@ namespace Umbraco.Web.Editors
private readonly IContentService _contentService;
private readonly WebSecurity _security;
private readonly IUserService _userService;
+ private readonly IEntityService _entityService;
public ContentPostValidateAttribute()
{
}
- public ContentPostValidateAttribute(IContentService contentService, IUserService userService, WebSecurity security)
+ public ContentPostValidateAttribute(IContentService contentService, IUserService userService, IEntityService entityService, WebSecurity security)
{
if (contentService == null) throw new ArgumentNullException("contentService");
if (userService == null) throw new ArgumentNullException("userService");
+ if (entityService == null) throw new ArgumentNullException("entityService");
if (security == null) throw new ArgumentNullException("security");
_contentService = contentService;
_userService = userService;
+ _entityService = entityService;
_security = security;
}
@@ -53,6 +56,11 @@ namespace Umbraco.Web.Editors
get { return _userService ?? Current.Services.UserService; }
}
+ private IEntityService EntityService
+ {
+ get { return _entityService ?? ApplicationContext.Current.Services.EntityService; }
+ }
+
public override bool AllowMultiple
{
get { return true; }
@@ -140,7 +148,8 @@ namespace Umbraco.Web.Editors
actionContext.Request.Properties,
Security.CurrentUser,
UserService,
- ContentService,
+ ContentService,
+ EntityService,
contentIdToCheck,
permissionToCheck.ToArray(),
contentToCheck) == false)
diff --git a/src/Umbraco.Web/Editors/ContentTypeController.cs b/src/Umbraco.Web/Editors/ContentTypeController.cs
index 6fdafddc87..ff21203d68 100644
--- a/src/Umbraco.Web/Editors/ContentTypeController.cs
+++ b/src/Umbraco.Web/Editors/ContentTypeController.cs
@@ -279,7 +279,18 @@ namespace Umbraco.Web.Editors
{
basic.Name = localizedTextService.UmbracoDictionaryTranslate(basic.Name);
basic.Description = localizedTextService.UmbracoDictionaryTranslate(basic.Description);
- }
+ }
+
+ //map the blueprints
+ var blueprints = Services.ContentService.GetBlueprintsForContentTypes(types.Select(x => x.Id).ToArray()).ToArray();
+ foreach (var basic in basics)
+ {
+ var docTypeBluePrints = blueprints.Where(x => x.ContentTypeId == (int) basic.Id).ToArray();
+ foreach (var blueprint in docTypeBluePrints)
+ {
+ basic.Blueprints[blueprint.Id] = blueprint.Name;
+ }
+ }
return basics;
}
diff --git a/src/Umbraco.Web/Editors/CurrentUserController.cs b/src/Umbraco.Web/Editors/CurrentUserController.cs
index 6a07c150fa..1dc431b069 100644
--- a/src/Umbraco.Web/Editors/CurrentUserController.cs
+++ b/src/Umbraco.Web/Editors/CurrentUserController.cs
@@ -1,11 +1,13 @@
-using System;
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Web.Http;
using Umbraco.Core.Services;
+using Umbraco.Core.Services;
using Umbraco.Web.Models;
using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Mvc;
using Umbraco.Web.WebApi;
+using Umbraco.Core.Security;
+using Umbraco.Web.WebApi.Filters;
using Constants = Umbraco.Core.Constants;
@@ -17,14 +19,54 @@ namespace Umbraco.Web.Editors
[PluginController("UmbracoApi")]
public class CurrentUserController : UmbracoAuthorizedJsonController
{
+
///
- /// Returns the configuration for the backoffice user membership provider - used to configure the change password dialog
+ /// When a user is invited and they click on the invitation link, they will be partially logged in
+ /// where they can set their username/password
///
+ ///
///
- public IDictionary GetMembershipProviderConfig()
+ ///
+ /// This only works when the user is logged in (partially)
+ ///
+ [WebApi.UmbracoAuthorize(requireApproval: false)]
+ [OverrideAuthorization]
+ public async Task PostSetInvitedUserPassword([FromBody]string newPassword)
{
- var provider = Core.Security.MembershipProviderExtensions.GetUsersMembershipProvider();
- return provider.GetConfiguration(Services.UserService); // fixme inject
+ var result = await UserManager.AddPasswordAsync(Security.GetUserId(), newPassword);
+
+ if (result.Succeeded == false)
+ {
+ //it wasn't successful, so add the change error to the model state, we've name the property alias _umb_password on the form
+ // so that is why it is being used here.
+ ModelState.AddModelError(
+ "value",
+ string.Join(", ", result.Errors));
+
+ throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState));
+ }
+
+ //They've successfully set their password, we can now update their user account to be approved
+ Security.CurrentUser.IsApproved = true;
+ Services.UserService.Save(Security.CurrentUser);
+
+ //now we can return their full object since they are now really logged into the back office
+ var userDisplay = Mapper.Map(Security.CurrentUser);
+ var httpContextAttempt = TryGetHttpContext();
+ if (httpContextAttempt.Success)
+ {
+ //set their remaining seconds
+ userDisplay.SecondsUntilTimeout = httpContextAttempt.Result.GetRemainingAuthSeconds();
+ }
+ return userDisplay;
+ }
+
+ [AppendUserModifiedHeader]
+ [FileUploadCleanupFilter(false)]
+ public async Task PostSetAvatar()
+ {
+ //borrow the logic from the user controller
+ return await UsersController.PostSetAvatarInternal(Request, Services.UserService, ApplicationContext.ApplicationCache.StaticCache, Security.GetUserId());
}
///
@@ -34,17 +76,11 @@ namespace Umbraco.Web.Editors
///
/// If the password is being reset it will return the newly reset password, otherwise will return an empty value
///
- public ModelWithNotifications PostChangePassword(ChangingPasswordModel data)
+ public async Task> PostChangePassword(ChangingPasswordModel data)
{
- var userProvider = Core.Security.MembershipProviderExtensions.GetUsersMembershipProvider();
+ var passwordChanger = new PasswordChanger(Logger, Services.UserService);
+ var passwordChangeResult = await passwordChanger.ChangePasswordWithIdentityAsync(Security.CurrentUser, data, ModelState, UserManager);
- //TODO: WE need to support this! - requires UI updates, etc...
- if (userProvider.RequiresQuestionAndAnswer)
- {
- throw new NotSupportedException("Currently the user editor does not support providers that have RequiresQuestionAndAnswer specified");
- }
-
- var passwordChangeResult = Members.ChangePassword(Security.CurrentUser.Username, data, userProvider);
if (passwordChangeResult.Success)
{
//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
@@ -53,12 +89,6 @@ namespace Umbraco.Web.Editors
return result;
}
- //it wasn't successful, so add the change error to the model state, we've name the property alias _umb_password on the form
- // so that is why it is being used here.
- ModelState.AddPropertyError(
- passwordChangeResult.Result.ChangeError,
- string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix));
-
throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState));
}
diff --git a/src/Umbraco.Web/Editors/DashboardController.cs b/src/Umbraco.Web/Editors/DashboardController.cs
index 0842ae7800..949a2752a9 100644
--- a/src/Umbraco.Web/Editors/DashboardController.cs
+++ b/src/Umbraco.Web/Editors/DashboardController.cs
@@ -36,13 +36,12 @@ namespace Umbraco.Web.Editors
throw new HttpResponseException(HttpStatusCode.InternalServerError);
var user = Security.CurrentUser;
- var userType = user.UserType.Alias;
var allowedSections = string.Join(",", user.AllowedSections);
var language = user.Language;
var version = UmbracoVersion.SemanticVersion.ToSemanticString();
- var url = string.Format(baseUrl + "{0}?section={0}&type={1}&allowed={2}&lang={3}&version={4}", section, userType, allowedSections, language, version);
- var key = "umbraco-dynamic-dashboard-" + userType + language + allowedSections.Replace(",", "-") + section;
+ var url = string.Format(baseUrl + "{0}?section={0}&allowed={1}&lang={2}&version={3}", section, allowedSections, language, version);
+ var key = "umbraco-dynamic-dashboard-" + language + allowedSections.Replace(",", "-") + section;
var content = ApplicationCache.RuntimeCache.GetCacheItem(key);
var result = new JObject();
diff --git a/src/Umbraco.Web/Editors/DashboardSecurity.cs b/src/Umbraco.Web/Editors/DashboardSecurity.cs
index 0f5f715296..c7e8852a16 100644
--- a/src/Umbraco.Web/Editors/DashboardSecurity.cs
+++ b/src/Umbraco.Web/Editors/DashboardSecurity.cs
@@ -61,45 +61,57 @@ namespace Umbraco.Web.Editors
{
var allowedSoFar = false;
- //Check if this item as any grant-by-section arguments, if so check if the user has access to any of the sections approved, if so they will
- // be allowed to see it (so far)
- if (grantedBySectionTypes.Any())
+ // if there's no grantBySection or grant rules defined - we allow access so far and skip to checking deny rules
+ if (grantedBySectionTypes.Any() == false && grantedTypes.Any() == false)
{
- var allowedApps = sectionService.GetAllowedSections(Convert.ToInt32(user.Id))
- .Select(x => x.Alias)
- .ToArray();
-
- var allApprovedSections = grantedBySectionTypes.SelectMany(g => g.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)).ToArray();
- if (allApprovedSections.Any(allowedApps.Contains))
- {
- allowedSoFar = true;
- }
- }
-
- //Check if this item as any grant arguments, if so check if the user is one of the user types approved, if so they will
- // be allowed to see it (so far)
- if (grantedTypes.Any())
- {
- var allApprovedUserTypes = grantedTypes.SelectMany(g => g.Value.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries)).ToArray();
- if (allApprovedUserTypes.InvariantContains(user.UserType.Alias))
- {
- allowedSoFar = true;
- }
- }
- else
- {
- //if there are not explicit grant types then everyone is allowed so far and we'll only disallow on a deny basis
allowedSoFar = true;
}
+ // else we check the rules and only allow if one matches
+ else
+ {
+ // check if this item has any grant-by-section arguments.
+ // if so check if the user has access to any of the sections approved, if so they will be allowed to see it (so far)
+ if (grantedBySectionTypes.Any())
+ {
+ var allowedApps = sectionService.GetAllowedSections(Convert.ToInt32(user.Id))
+ .Select(x => x.Alias)
+ .ToArray();
- //Check if this item as any deny arguments, if so check if the user is one of the user types approved, if so they will
+ var allApprovedSections = grantedBySectionTypes.SelectMany(g => g.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)).ToArray();
+ if (allApprovedSections.Any(allowedApps.Contains))
+ {
+ allowedSoFar = true;
+ }
+ }
+
+ // if not already granted access, check if this item as any grant arguments.
+ // if so check if the user is in one of the user groups approved, if so they will be allowed to see it (so far)
+ if (allowedSoFar == false && grantedTypes.Any())
+ {
+ var allApprovedUserTypes = grantedTypes.SelectMany(g => g.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)).ToArray();
+ foreach (var userGroup in user.Groups)
+ {
+ if (allApprovedUserTypes.InvariantContains(userGroup.Alias))
+ {
+ allowedSoFar = true;
+ break;
+ }
+ }
+ }
+ }
+
+ // check if this item has any deny arguments, if so check if the user is in one of the denied user groups, if so they will
// be denied to see it no matter what
if (denyTypes.Any())
{
var allDeniedUserTypes = denyTypes.SelectMany(g => g.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)).ToArray();
- if (allDeniedUserTypes.InvariantContains(user.UserType.Alias))
+ foreach (var userGroup in user.Groups)
{
- allowedSoFar = false;
+ if (allDeniedUserTypes.InvariantContains(userGroup.Alias))
+ {
+ allowedSoFar = false;
+ break;
+ }
}
}
diff --git a/src/Umbraco.Web/Editors/DataTypeController.cs b/src/Umbraco.Web/Editors/DataTypeController.cs
index f969b97ec5..77c392bf8c 100644
--- a/src/Umbraco.Web/Editors/DataTypeController.cs
+++ b/src/Umbraco.Web/Editors/DataTypeController.cs
@@ -20,6 +20,7 @@ using Umbraco.Web.Composing;
namespace Umbraco.Web.Editors
{
+
///
/// The API controller used for editing data types
///
diff --git a/src/Umbraco.Web/Editors/DataTypeValidateAttribute.cs b/src/Umbraco.Web/Editors/DataTypeValidateAttribute.cs
index 8dad002285..863a30cb70 100644
--- a/src/Umbraco.Web/Editors/DataTypeValidateAttribute.cs
+++ b/src/Umbraco.Web/Editors/DataTypeValidateAttribute.cs
@@ -44,7 +44,7 @@ namespace Umbraco.Web.Editors
var dataType = (DataTypeSave)actionContext.ActionArguments["dataType"];
dataType.Name = dataType.Name.CleanForXss('[', ']', '(', ')', ':');
- dataType.Alias = dataType.Name.CleanForXss('[', ']', '(', ')', ':');
+ dataType.Alias = dataType.Alias == null ? dataType.Name : dataType.Alias.CleanForXss('[', ']', '(', ')', ':');
//Validate that the property editor exists
var propertyEditor = Current.PropertyEditors[dataType.SelectedEditor];
diff --git a/src/Umbraco.Web/Editors/EntityController.cs b/src/Umbraco.Web/Editors/EntityController.cs
index 5fb656cf29..f5f18e995e 100644
--- a/src/Umbraco.Web/Editors/EntityController.cs
+++ b/src/Umbraco.Web/Editors/EntityController.cs
@@ -20,8 +20,9 @@ using Examine;
using System.Text.RegularExpressions;
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
using System.Web.Http.Controllers;
-using Examine.LuceneEngine;
using Umbraco.Core.Xml;
+using Umbraco.Web.Search;
+using Umbraco.Web.Trees;
namespace Umbraco.Web.Editors
{
@@ -54,6 +55,8 @@ namespace Umbraco.Web.Editors
}
}
+ private readonly UmbracoTreeSearcher _treeSearcher = new UmbracoTreeSearcher();
+
///
/// Returns an Umbraco alias given a string
///
@@ -104,42 +107,40 @@ namespace Umbraco.Web.Editors
///
/// The reason a user is allowed to search individual entity types that they are not allowed to edit is because those search
/// methods might be used in things like pickers in the content editor.
- ///
+ ///
[HttpGet]
- public IEnumerable SearchAll(string query)
+ public IDictionary SearchAll(string query)
{
+ var result = new Dictionary();
+
if (string.IsNullOrEmpty(query))
- return Enumerable.Empty();
-
+ return result;
+
var allowedSections = Security.CurrentUser.AllowedSections.ToArray();
-
- var result = new List();
-
- if (allowedSections.InvariantContains(Constants.Applications.Content))
+ var searchableTrees = SearchableTreeResolver.Current.GetSearchableTrees();
+
+ foreach (var searchableTree in searchableTrees)
{
- result.Add(new EntityTypeSearchResult
+ if (allowedSections.Contains(searchableTree.Value.AppAlias))
+ {
+ var tree = Services.ApplicationTreeService.GetByAlias(searchableTree.Key);
+ if (tree == null) continue; //shouldn't occur
+ #error why cannot we use a collectino?
+ var searchableTreeAttribute = searchableTree.Value.SearchableTree.GetType().GetCustomAttribute(false);
+ var treeAttribute = tree.GetTreeAttribute();
+
+ long total;
+
+ result[treeAttribute.GetRootNodeDisplayName(Services.TextService)] = new TreeSearchResult
{
- Results = ExamineSearch(query, UmbracoEntityTypes.Document),
- EntityType = UmbracoEntityTypes.Document.ToString()
- });
- }
- if (allowedSections.InvariantContains(Constants.Applications.Media))
- {
- result.Add(new EntityTypeSearchResult
- {
- Results = ExamineSearch(query, UmbracoEntityTypes.Media),
- EntityType = UmbracoEntityTypes.Media.ToString()
- });
- }
- if (allowedSections.InvariantContains(Constants.Applications.Members))
- {
- result.Add(new EntityTypeSearchResult
- {
- Results = ExamineSearch(query, UmbracoEntityTypes.Member),
- EntityType = UmbracoEntityTypes.Member.ToString()
- });
-
- }
+ Results = searchableTree.Value.SearchableTree.Search(query, 200, 0, out total),
+ TreeAlias = searchableTree.Key,
+ AppAlias = searchableTree.Value.AppAlias,
+ JsFormatterService = searchableTreeAttribute == null ? "" : searchableTreeAttribute.ServiceName,
+ JsFormatterMethod = searchableTreeAttribute == null ? "" : searchableTreeAttribute.MethodName
+ };
+ }
+ }
return result;
}
@@ -459,8 +460,8 @@ namespace Umbraco.Web.Editors
//the EntityService cannot search members of a certain type, this is currently not supported and would require
//quite a bit of plumbing to do in the Services/Repository, we'll revert to a paged search
- int total;
- var searchResult = ExamineSearch(filter ?? "", type, pageSize, pageNumber - 1, out total, id);
+ long total;
+ var searchResult = _treeSearcher.ExamineSearch(Umbraco, filter ?? "", type, pageSize, pageNumber - 1, out total, id);
return new PagedResult(total, pageNumber, pageSize)
{
@@ -543,11 +544,32 @@ namespace Umbraco.Web.Editors
var objectType = ConvertToObjectType(type);
if (objectType.HasValue)
{
+ IEnumerable entities;
long totalRecords;
- //if it's from root, don't return recycled
- var entities = id == Constants.System.Root
- ? Services.EntityService.GetPagedDescendantsFromRoot(objectType.Value, pageNumber - 1, pageSize, out totalRecords, orderBy, orderDirection, filter, includeTrashed:false)
- : Services.EntityService.GetPagedDescendants(id, objectType.Value, pageNumber - 1, pageSize, out totalRecords, orderBy, orderDirection, filter);
+
+ if (id == Constants.System.Root)
+ {
+ // root is special: we reduce it to start nodes
+
+ int[] aids = null;
+ switch (type)
+ {
+ case UmbracoEntityTypes.Document:
+ aids = Security.CurrentUser.CalculateContentStartNodeIds(Services.EntityService);
+ break;
+ case UmbracoEntityTypes.Media:
+ aids = Security.CurrentUser.CalculateMediaStartNodeIds(Services.EntityService);
+ break;
+ }
+
+ entities = aids == null || aids.Contains(Constants.System.Root)
+ ? Services.EntityService.GetPagedDescendantsFromRoot(objectType.Value, pageNumber - 1, pageSize, out totalRecords, orderBy, orderDirection, filter, includeTrashed: false)
+ : Services.EntityService.GetPagedDescendants(aids, objectType.Value, pageNumber - 1, pageSize, out totalRecords, orderBy, orderDirection, filter);
+ }
+ else
+ {
+ entities = Services.EntityService.GetPagedDescendants(id, objectType.Value, pageNumber - 1, pageSize, out totalRecords, orderBy, orderDirection, filter);
+ }
if (totalRecords == 0)
{
@@ -588,311 +610,14 @@ namespace Umbraco.Web.Editors
///
///
///
- private IEnumerable ExamineSearch(string query, UmbracoEntityTypes entityType, string searchFrom = null)
+ private IEnumerable ExamineSearch(string query, UmbracoEntityTypes entityType, string searchFrom = null)
{
- int total;
- return ExamineSearch(query, entityType, 200, 0, out total, searchFrom);
+ long total;
+ return _treeSearcher.ExamineSearch(Umbraco, query, entityType, 200, 0, out total, searchFrom);
}
- ///
- /// Searches for results based on the entity type
- ///
- ///
- ///
- ///
- ///
- /// A starting point for the search, generally a node id, but for members this is a member type alias
- ///
- ///
- ///
- ///
- private IEnumerable ExamineSearch(string query, UmbracoEntityTypes entityType, int pageSize, int pageIndex, out int totalFound, string searchFrom = null)
- {
- //TODO: We need to update this to support paging
+
- var sb = new StringBuilder();
-
- string type;
-
- var fields = new[] { "id", "__NodeId" };
-
- //TODO: WE should really just allow passing in a lucene raw query
- switch (entityType)
- {
- case UmbracoEntityTypes.Member:
-
- type = "member";
- fields = new[] { "id", "__NodeId", "email", "loginName"};
- if (searchFrom != null && searchFrom != Constants.Conventions.MemberTypes.AllMembersListId && searchFrom.Trim() != "-1")
- {
- sb.Append("+__NodeTypeAlias:");
- sb.Append(searchFrom);
- sb.Append(" ");
- }
- break;
- case UmbracoEntityTypes.Media:
- type = "media";
-
- var mediaSearchFrom = int.MinValue;
-
- if (Security.CurrentUser.StartMediaId > 0 ||
- //if searchFrom is specified and it is greater than 0
- (searchFrom != null && int.TryParse(searchFrom, out mediaSearchFrom) && mediaSearchFrom > 0))
- {
- sb.Append("+__Path: \\-1*\\,");
- sb.Append(mediaSearchFrom > 0
- ? mediaSearchFrom.ToString(CultureInfo.InvariantCulture)
- : Security.CurrentUser.StartMediaId.ToString(CultureInfo.InvariantCulture));
- sb.Append("\\,* ");
- }
- break;
- case UmbracoEntityTypes.Document:
- type = "content";
-
- var contentSearchFrom = int.MinValue;
-
- if (Security.CurrentUser.StartContentId > 0 ||
- //if searchFrom is specified and it is greater than 0
- (searchFrom != null && int.TryParse(searchFrom, out contentSearchFrom) && contentSearchFrom > 0))
- {
- sb.Append("+__Path: \\-1*\\,");
- sb.Append(contentSearchFrom > 0
- ? contentSearchFrom.ToString(CultureInfo.InvariantCulture)
- : Security.CurrentUser.StartContentId.ToString(CultureInfo.InvariantCulture));
- sb.Append("\\,* ");
- }
- break;
- default:
- throw new NotSupportedException("The " + typeof(EntityController) + " currently does not support searching against object type " + entityType);
- }
-
- var internalSearcher = ExamineManager.Instance.GetSearcher(Constants.Examine.InternalIndexer);
-
- //build a lucene query:
- // the __nodeName will be boosted 10x without wildcards
- // then __nodeName will be matched normally with wildcards
- // the rest will be normal without wildcards
-
-
- //check if text is surrounded by single or double quotes, if so, then exact match
- var surroundedByQuotes = Regex.IsMatch(query, "^\".*?\"$")
- || Regex.IsMatch(query, "^\'.*?\'$");
-
- if (surroundedByQuotes)
- {
- //strip quotes, escape string, the replace again
- query = query.Trim('\"', '\'');
-
- query = Lucene.Net.QueryParsers.QueryParser.Escape(query);
-
- //nothing to search
- if (searchFrom.IsNullOrWhiteSpace() && query.IsNullOrWhiteSpace())
- {
- totalFound = 0;
- return new List();
- }
-
- //update the query with the query term
- if (query.IsNullOrWhiteSpace() == false)
- {
- //add back the surrounding quotes
- query = string.Format("{0}{1}{0}", "\"", query);
-
- //node name exactly boost x 10
- sb.Append("+(__nodeName: (");
- sb.Append(query.ToLower());
- sb.Append(")^10.0 ");
-
- foreach (var f in fields)
- {
- //additional fields normally
- sb.Append(f);
- sb.Append(": (");
- sb.Append(query);
- sb.Append(") ");
- }
-
- sb.Append(") ");
- }
- }
- else
- {
- var trimmed = query.Trim(new[] {'\"', '\''});
-
- //nothing to search
- if (searchFrom.IsNullOrWhiteSpace() && trimmed.IsNullOrWhiteSpace())
- {
- totalFound = 0;
- return new List();
- }
-
- //update the query with the query term
- if (trimmed.IsNullOrWhiteSpace() == false)
- {
- query = Lucene.Net.QueryParsers.QueryParser.Escape(query);
-
- var querywords = query.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
-
- //node name exactly boost x 10
- sb.Append("+(__nodeName:");
- sb.Append("\"");
- sb.Append(query.ToLower());
- sb.Append("\"");
- sb.Append("^10.0 ");
-
- //node name normally with wildcards
- sb.Append(" __nodeName:");
- sb.Append("(");
- foreach (var w in querywords)
- {
- sb.Append(w.ToLower());
- sb.Append("* ");
- }
- sb.Append(") ");
-
-
- foreach (var f in fields)
- {
- //additional fields normally
- sb.Append(f);
- sb.Append(":");
- sb.Append("(");
- foreach (var w in querywords)
- {
- sb.Append(w.ToLower());
- sb.Append("* ");
- }
- sb.Append(")");
- sb.Append(" ");
- }
-
- sb.Append(") ");
- }
- }
-
- //must match index type
- sb.Append("+__IndexType:");
- sb.Append(type);
-
- var raw = internalSearcher.CreateSearchCriteria().RawQuery(sb.ToString());
-
- // fixme - ISearcher has not been updated in Examine for v8?
- throw new NotImplementedException();
- //var result = internalSearcher
- // //only return the number of items specified to read up to the amount of records to fill from 0 -> the number of items on the page requested
- // .Search(raw, pageSize * (pageIndex + 1));
- var result = new SR();
-
- totalFound = result.TotalItemCount;
-
- var pagedResult = result.Skip(pageIndex);
-
- switch (entityType)
- {
- case UmbracoEntityTypes.Member:
- return MemberFromSearchResults(pagedResult.ToArray());
- case UmbracoEntityTypes.Media:
- return MediaFromSearchResults(pagedResult);
- case UmbracoEntityTypes.Document:
- return ContentFromSearchResults(pagedResult);
- default:
- throw new NotSupportedException("The " + typeof(EntityController) + " currently does not support searching against object type " + entityType);
- }
- }
-
- private class SR : ISearchResults
- {
- public IEnumerator GetEnumerator()
- {
- throw new NotImplementedException();
- }
-
- IEnumerator IEnumerable.GetEnumerator()
- {
- return GetEnumerator();
- }
-
- public IEnumerable Skip(int skip)
- {
- throw new NotImplementedException();
- }
-
- public int TotalItemCount { get; }
- }
-
- ///
- /// Returns a collection of entities for media based on search results
- ///
- ///
- ///
- private IEnumerable MemberFromSearchResults(SearchResult[] results)
- {
- var mapped = Mapper.Map>(results).ToArray();
- //add additional data
- foreach (var m in mapped)
- {
- //if no icon could be mapped, it will be set to document, so change it to picture
- if (m.Icon == "icon-document")
- {
- m.Icon = "icon-user";
- }
-
- var searchResult = results.First(x => x.LongId.ToInvariantString() == m.Id.ToString());
- if (searchResult.Fields.ContainsKey("email") && searchResult.Fields["email"] != null)
- {
- m.AdditionalData["Email"] = results.First(x => x.LongId.ToInvariantString() == m.Id.ToString()).Fields["email"];
- }
- if (searchResult.Fields.ContainsKey("__key") && searchResult.Fields["__key"] != null)
- {
- Guid key;
- if (Guid.TryParse(searchResult.Fields["__key"], out key))
- {
- m.Key = key;
- }
- }
- }
- return mapped;
- }
-
- ///
- /// Returns a collection of entities for media based on search results
- ///
- ///
- ///
- private IEnumerable MediaFromSearchResults(IEnumerable results)
- {
- var mapped = Mapper.Map>(results).ToArray();
- //add additional data
- foreach (var m in mapped)
- {
- //if no icon could be mapped, it will be set to document, so change it to picture
- if (m.Icon == "icon-document")
- {
- m.Icon = "icon-picture";
- }
- }
- return mapped;
- }
-
- ///
- /// Returns a collection of entities for content based on search results
- ///
- ///
- ///
- private IEnumerable ContentFromSearchResults(IEnumerable results)
- {
- var mapped = Mapper.Map>(results).ToArray();
- //add additional data
- foreach (var m in mapped)
- {
- var intId = m.Id.TryConvertTo();
- if (intId.Success)
- {
- m.AdditionalData["Url"] = Umbraco.NiceUrl(intId.Result);
- }
- }
- return mapped;
- }
private IEnumerable GetResultForChildren(int id, UmbracoEntityTypes entityType)
{
@@ -926,10 +651,43 @@ namespace Umbraco.Web.Editors
var ids = Services.EntityService.Get(id).Path.Split(',').Select(int.Parse).Distinct().ToArray();
- return Services.EntityService.GetAll(objectType.Value, ids)
- .WhereNotNull()
- .OrderBy(x => x.Level)
- .Select(Mapper.Map);
+ int[] aids = null;
+ switch (entityType)
+ {
+ case UmbracoEntityTypes.Document:
+ aids = Security.CurrentUser.CalculateContentStartNodeIds(Services.EntityService);
+ break;
+ case UmbracoEntityTypes.Media:
+ aids = Security.CurrentUser.CalculateMediaStartNodeIds(Services.EntityService);
+ break;
+ }
+
+ if (aids != null)
+ {
+ var lids = new List();
+ var ok = false;
+ foreach (var i in ids)
+ {
+ if (ok)
+ {
+ lids.Add(i);
+ continue;
+ }
+ if (aids.Contains(i))
+ {
+ lids.Add(i);
+ ok = true;
+ }
+ }
+ ids = lids.ToArray();
+ }
+
+ return ids.Length == 0
+ ? Enumerable.Empty()
+ : Services.EntityService.GetAll(objectType.Value, ids)
+ .WhereNotNull()
+ .OrderBy(x => x.Level)
+ .Select(Mapper.Map);
}
//now we need to convert the unknown ones
switch (entityType)
diff --git a/src/Umbraco.Web/Editors/GravatarController.cs b/src/Umbraco.Web/Editors/GravatarController.cs
deleted file mode 100644
index 2dda8a448a..0000000000
--- a/src/Umbraco.Web/Editors/GravatarController.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-using System;
-using System.Net;
-using Umbraco.Core;
-using Umbraco.Web.Mvc;
-
-namespace Umbraco.Web.Editors
-{
- ///
- /// API controller used for getting Gravatar urls
- ///
- [PluginController("UmbracoApi")]
- public class GravatarController : UmbracoAuthorizedJsonController
- {
- public string GetCurrentUserGravatarUrl()
- {
- var userService = Services.UserService;
- var user = userService.GetUserById(UmbracoContext.Security.CurrentUser.Id);
- var gravatarHash = user.Email.ToMd5();
- var gravatarUrl = "https://www.gravatar.com/avatar/" + gravatarHash;
-
- // Test if we can reach this URL, will fail when there's network or firewall errors
- var request = (HttpWebRequest)WebRequest.Create(gravatarUrl);
- // Require response within 10 seconds
- request.Timeout = 10000;
- try
- {
- using ((HttpWebResponse)request.GetResponse()) { }
- }
- catch (Exception)
- {
- // There was an HTTP or other error, return an null instead
- return null;
- }
-
- return gravatarUrl;
- }
- }
-}
diff --git a/src/Umbraco.Web/Editors/IsCurrentUserModelFilterAttribute.cs b/src/Umbraco.Web/Editors/IsCurrentUserModelFilterAttribute.cs
new file mode 100644
index 0000000000..a7164b3c65
--- /dev/null
+++ b/src/Umbraco.Web/Editors/IsCurrentUserModelFilterAttribute.cs
@@ -0,0 +1,55 @@
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Web.Http.Filters;
+using Umbraco.Web.Models.ContentEditing;
+
+namespace Umbraco.Web.Editors
+{
+ ///
+ /// This sets the IsCurrentUser property on any outgoing model or any collection of models
+ ///
+ internal class IsCurrentUserModelFilterAttribute : ActionFilterAttribute
+ {
+ public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
+ {
+ if (actionExecutedContext.Response == null) return;
+
+ var user = UmbracoContext.Current.Security.CurrentUser;
+ if (user == null) return;
+
+ var objectContent = actionExecutedContext.Response.Content as ObjectContent;
+ if (objectContent != null)
+ {
+ var model = objectContent.Value as UserBasic;
+ if (model != null)
+ {
+ model.IsCurrentUser = (int) model.Id == user.Id;
+ }
+ else
+ {
+ var collection = objectContent.Value as IEnumerable;
+ if (collection != null)
+ {
+ foreach (var userBasic in collection)
+ {
+ userBasic.IsCurrentUser = (int) userBasic.Id == user.Id;
+ }
+ }
+ else
+ {
+ var paged = objectContent.Value as UsersController.PagedUserResult;
+ if (paged != null && paged.Items != null)
+ {
+ foreach (var userBasic in paged.Items)
+ {
+ userBasic.IsCurrentUser = (int)userBasic.Id == user.Id;
+ }
+ }
+ }
+ }
+ }
+
+ base.OnActionExecuted(actionExecutedContext);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs
index 608480ee70..1a7b19f9d8 100644
--- a/src/Umbraco.Web/Editors/MediaController.cs
+++ b/src/Umbraco.Web/Editors/MediaController.cs
@@ -234,6 +234,13 @@ namespace Umbraco.Web.Editors
}
#region GetChildren
+
+ private int[] _userStartNodes;
+ protected int[] UserStartNodes
+ {
+ get { return _userStartNodes ?? (_userStartNodes = Security.CurrentUser.CalculateMediaStartNodeIds(Services.EntityService)); }
+ }
+
///
/// Returns the child media objects - using the entity INT id
///
@@ -246,6 +253,25 @@ namespace Umbraco.Web.Editors
bool orderBySystemField = true,
string filter = "")
{
+ //if a request is made for the root node data but the user's start node is not the default, then
+ // we need to return their start nodes
+ if (id == Constants.System.Root && UserStartNodes.Length > 0 && UserStartNodes.Contains(Constants.System.Root) == false)
+ {
+ if (pageNumber > 0)
+ return new PagedResult>(0, 0, 0);
+ var nodes = Services.MediaService.GetByIds(UserStartNodes).ToArray();
+ if (nodes.Length == 0)
+ return new PagedResult>(0, 0, 0);
+ if (pageSize < nodes.Length) pageSize = nodes.Length; // bah
+ var pr = new PagedResult>(nodes.Length, pageNumber, pageSize)
+ {
+ Items = nodes.Select(Mapper.Map>)
+ };
+ return pr;
+ }
+
+ // else proceed as usual
+
long totalChildren;
IMedia[] children;
if (pageNumber > 0 && pageSize > 0)
@@ -649,7 +675,9 @@ namespace Umbraco.Web.Editors
if (CheckPermissions(
new Dictionary(),
Security.CurrentUser,
- Services.MediaService, parentId) == false)
+ Services.MediaService,
+ Services.EntityService,
+ parentId) == false)
{
return Request.CreateResponse(
HttpStatusCode.Forbidden,
@@ -861,11 +889,17 @@ namespace Umbraco.Web.Editors
/// The storage to add the content item to so it can be reused
///
///
+ ///
/// The content to lookup, if the contentItem is not specified
/// Specifies the already resolved content item to check against, setting this ignores the nodeId
///
- internal static bool CheckPermissions(IDictionary storage, IUser user, IMediaService mediaService, int nodeId, IMedia media = null)
+ internal static bool CheckPermissions(IDictionary storage, IUser user, IMediaService mediaService, IEntityService entityService, int nodeId, IMedia media = null)
{
+ if (storage == null) throw new ArgumentNullException("storage");
+ if (user == null) throw new ArgumentNullException("user");
+ if (mediaService == null) throw new ArgumentNullException("mediaService");
+ if (entityService == null) throw new ArgumentNullException("entityService");
+
if (media == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinMedia)
{
media = mediaService.GetById(nodeId);
@@ -877,19 +911,13 @@ namespace Umbraco.Web.Editors
if (media == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinMedia)
{
throw new HttpResponseException(HttpStatusCode.NotFound);
- }
+ }
var hasPathAccess = (nodeId == Constants.System.Root)
- ? UserExtensions.HasPathAccess(
- Constants.System.Root.ToInvariantString(),
- user.StartMediaId,
- Constants.System.RecycleBinMedia)
- : (nodeId == Constants.System.RecycleBinMedia)
- ? UserExtensions.HasPathAccess(
- Constants.System.RecycleBinMedia.ToInvariantString(),
- user.StartMediaId,
- Constants.System.RecycleBinMedia)
- : user.HasPathAccess(media);
+ ? user.HasMediaRootAccess(entityService)
+ : (nodeId == Constants.System.RecycleBinMedia)
+ ? user.HasMediaBinAccess(entityService)
+ : user.HasPathAccess(media, entityService);
return hasPathAccess;
}
diff --git a/src/Umbraco.Web/Editors/MediaPostValidateAttribute.cs b/src/Umbraco.Web/Editors/MediaPostValidateAttribute.cs
index b6b94d694d..7784129f8a 100644
--- a/src/Umbraco.Web/Editors/MediaPostValidateAttribute.cs
+++ b/src/Umbraco.Web/Editors/MediaPostValidateAttribute.cs
@@ -20,17 +20,19 @@ namespace Umbraco.Web.Editors
internal sealed class MediaPostValidateAttribute : ActionFilterAttribute
{
private readonly IMediaService _mediaService;
+ private readonly IEntityService _entityService;
private readonly WebSecurity _security;
public MediaPostValidateAttribute()
{
}
- public MediaPostValidateAttribute(IMediaService mediaService, WebSecurity security)
+ public MediaPostValidateAttribute(IMediaService mediaService, IEntityService entityService, WebSecurity security)
{
if (mediaService == null) throw new ArgumentNullException("mediaService");
if (security == null) throw new ArgumentNullException("security");
_mediaService = mediaService;
+ _entityService = entityService;
_security = security;
}
@@ -39,6 +41,11 @@ namespace Umbraco.Web.Editors
get { return _mediaService ?? Current.Services.MediaService; }
}
+ private IEntityService EntityService
+ {
+ get { return _entityService ?? ApplicationContext.Current.Services.EntityService; }
+ }
+
private WebSecurity Security
{
get { return _security ?? UmbracoContext.Current.Security; }
@@ -82,6 +89,7 @@ namespace Umbraco.Web.Editors
actionContext.Request.Properties,
Security.CurrentUser,
MediaService,
+ EntityService,
contentIdToCheck,
contentToCheck) == false)
{
diff --git a/src/Umbraco.Web/Editors/MediaTypeController.cs b/src/Umbraco.Web/Editors/MediaTypeController.cs
index 3907944154..24e5b0a3fa 100644
--- a/src/Umbraco.Web/Editors/MediaTypeController.cs
+++ b/src/Umbraco.Web/Editors/MediaTypeController.cs
@@ -30,7 +30,7 @@ namespace Umbraco.Web.Editors
[PluginController("UmbracoApi")]
[UmbracoTreeAuthorize(Constants.Trees.MediaTypes)]
[EnableOverrideAuthorization]
- [MediaTypeControllerControllerConfigurationAttribute]
+ [MediaTypeControllerControllerConfiguration]
public class MediaTypeController : ContentTypeControllerBase
{
///
@@ -213,7 +213,7 @@ namespace Umbraco.Web.Editors
basic.Description = TranslateItem(basic.Description);
}
- return basics;
+ return basics.OrderBy(x => x.Name);
}
///
diff --git a/src/Umbraco.Web/Editors/MemberController.cs b/src/Umbraco.Web/Editors/MemberController.cs
index f6b26bb436..90b9768190 100644
--- a/src/Umbraco.Web/Editors/MemberController.cs
+++ b/src/Umbraco.Web/Editors/MemberController.cs
@@ -205,8 +205,10 @@ namespace Umbraco.Web.Editors
throw new HttpResponseException(HttpStatusCode.NotFound);
}
+ var provider = Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider();
+
emptyContent = new Member(contentType);
- emptyContent.AdditionalData["NewPassword"] = Membership.GeneratePassword(Membership.MinRequiredPasswordLength, Membership.MinRequiredNonAlphanumericCharacters);
+ emptyContent.AdditionalData["NewPassword"] = Membership.GeneratePassword(provider.MinRequiredPasswordLength, provider.MinRequiredNonAlphanumericCharacters);
return Mapper.Map(emptyContent);
case MembershipScenario.CustomProviderWithUmbracoLink:
//TODO: Support editing custom properties for members with a custom membership provider here.
diff --git a/src/Umbraco.Web/Editors/PackageInstallController.cs b/src/Umbraco.Web/Editors/PackageInstallController.cs
index 3e08852bfb..17b46cc849 100644
--- a/src/Umbraco.Web/Editors/PackageInstallController.cs
+++ b/src/Umbraco.Web/Editors/PackageInstallController.cs
@@ -536,6 +536,26 @@ namespace Umbraco.Web.Editors
var ins = new global::umbraco.cms.businesslogic.packager.Installer(Security.CurrentUser.Id);
ins.LoadConfig(IOHelper.MapPath(model.TemporaryDirectoryPath));
ins.InstallFiles(model.Id, IOHelper.MapPath(model.TemporaryDirectoryPath));
+
+ //set a restarting marker and reset the app pool
+ ApplicationContext.RestartApplicationPool(Request.TryGetHttpContext().Result);
+
+ model.IsRestarting = true;
+
+ return model;
+ }
+
+ [HttpPost]
+ public PackageInstallModel CheckRestart(PackageInstallModel model)
+ {
+ if (model.IsRestarting == false) return model;
+
+ //check for the key, if it's not there we're are restarted
+ if (Request.TryGetHttpContext().Result.Application.AllKeys.Contains("AppPoolRestarting") == false)
+ {
+ //reset it
+ model.IsRestarting = false;
+ }
return model;
}
diff --git a/src/Umbraco.Web/Editors/PasswordChanger.cs b/src/Umbraco.Web/Editors/PasswordChanger.cs
new file mode 100644
index 0000000000..ad62c0b0e9
--- /dev/null
+++ b/src/Umbraco.Web/Editors/PasswordChanger.cs
@@ -0,0 +1,240 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.Threading.Tasks;
+using System.Web.Http.ModelBinding;
+using System.Web.Security;
+using Umbraco.Core;
+using Umbraco.Core.Logging;
+using Umbraco.Core.Models.Identity;
+using Umbraco.Core.Security;
+using Umbraco.Core.Services;
+using Umbraco.Web.Models;
+using IUser = Umbraco.Core.Models.Membership.IUser;
+
+namespace Umbraco.Web.Editors
+{
+ internal class PasswordChanger
+ {
+ private readonly ILogger _logger;
+ private readonly IUserService _userService;
+
+ public PasswordChanger(ILogger logger, IUserService userService)
+ {
+ _logger = logger;
+ _userService = userService;
+ }
+
+ public async Task> ChangePasswordWithIdentityAsync(
+ IUser currentUser,
+ ChangingPasswordModel passwordModel,
+ ModelStateDictionary modelState,
+ BackOfficeUserManager userMgr)
+ {
+ if (passwordModel == null) throw new ArgumentNullException(nameof(passwordModel));
+ if (userMgr == null) throw new ArgumentNullException(nameof(userMgr));
+
+ //check if this identity implementation is powered by an underlying membership provider (it will be in most cases)
+ var membershipPasswordHasher = userMgr.PasswordHasher as IMembershipProviderPasswordHasher;
+
+ //check if this identity implementation is powered by an IUserAwarePasswordHasher (it will be by default in 7.7+ but not for upgrades)
+
+ if (membershipPasswordHasher != null && !(userMgr.PasswordHasher is IUserAwarePasswordHasher))
+ {
+ //if this isn't using an IUserAwarePasswordHasher, then fallback to the old way
+ if (membershipPasswordHasher.MembershipProvider.RequiresQuestionAndAnswer)
+ throw new NotSupportedException("Currently the user editor does not support providers that have RequiresQuestionAndAnswer specified");
+ return ChangePasswordWithMembershipProvider(currentUser.Username, passwordModel, membershipPasswordHasher.MembershipProvider);
+ }
+
+ //if we are here, then a IUserAwarePasswordHasher is available, however we cannot proceed in that case if for some odd reason
+ //the user has configured the membership provider to not be hashed. This will actually never occur because the BackOfficeUserManager
+ //will throw if it's not hashed, but we should make sure to check anyways (i.e. in case we want to unit test!)
+ if (membershipPasswordHasher != null && membershipPasswordHasher.MembershipProvider.PasswordFormat != MembershipPasswordFormat.Hashed)
+ {
+ throw new InvalidOperationException("The membership provider cannot have a password format of " + membershipPasswordHasher.MembershipProvider.PasswordFormat + " and be configured with secured hashed passwords");
+ }
+
+ //Are we resetting the password??
+ if (passwordModel.Reset.HasValue && passwordModel.Reset.Value)
+ {
+ //ok, we should be able to reset it
+ var resetToken = await userMgr.GeneratePasswordResetTokenAsync(currentUser.Id);
+ var newPass = userMgr.GeneratePassword();
+ var resetResult = await userMgr.ResetPasswordAsync(currentUser.Id, resetToken, newPass);
+
+ if (resetResult.Succeeded == false)
+ {
+ var errors = string.Join(". ", resetResult.Errors);
+ _logger.Warn($"Could not reset member password {errors}");
+ return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not reset password, errors: " + errors, new[] { "resetPassword" }) });
+ }
+
+ return Attempt.Succeed(new PasswordChangedModel { ResetPassword = newPass });
+ }
+
+ //we're not resetting it so we need to try to change it.
+
+ if (passwordModel.NewPassword.IsNullOrWhiteSpace())
+ {
+ return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Cannot set an empty password", new[] { "value" }) });
+ }
+
+ //we cannot arbitrarily change the password without knowing the old one and no old password was supplied - need to return an error
+ //TODO: What if the current user is admin? We should allow manually changing then?
+ if (passwordModel.OldPassword.IsNullOrWhiteSpace())
+ {
+ //if password retrieval is not enabled but there is no old password we cannot continue
+ return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password cannot be changed without the old password", new[] { "oldPassword" }) });
+ }
+
+ if (passwordModel.OldPassword.IsNullOrWhiteSpace() == false)
+ {
+ //if an old password is suplied try to change it
+ var changeResult = await userMgr.ChangePasswordAsync(currentUser.Id, passwordModel.OldPassword, passwordModel.NewPassword);
+ if (changeResult.Succeeded == false)
+ {
+ var errors = string.Join(". ", changeResult.Errors);
+ _logger.Warn($"Could not change member password {errors}");
+ return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, errors: " + errors, new[] { "value" }) });
+ }
+ return Attempt.Succeed(new PasswordChangedModel());
+ }
+
+ //We shouldn't really get here
+ return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, invalid information supplied", new[] { "value" }) });
+ }
+
+ ///
+ /// Changes password for a member/user given the membership provider and the password change model
+ ///
+ ///
+ ///
+ ///
+ ///
+ public Attempt ChangePasswordWithMembershipProvider(string username, ChangingPasswordModel passwordModel, MembershipProvider membershipProvider)
+ {
+ // YES! It is completely insane how many options you have to take into account based on the membership provider. yikes!
+
+ if (passwordModel == null) throw new ArgumentNullException(nameof(passwordModel));
+ if (membershipProvider == null) throw new ArgumentNullException(nameof(membershipProvider));
+
+ //Are we resetting the password??
+ if (passwordModel.Reset.HasValue && passwordModel.Reset.Value)
+ {
+ var canReset = membershipProvider.CanResetPassword(_userService);
+ if (canReset == false)
+ {
+ return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password reset is not enabled", new[] { "resetPassword" }) });
+ }
+ if (membershipProvider.RequiresQuestionAndAnswer && passwordModel.Answer.IsNullOrWhiteSpace())
+ {
+ return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password reset requires a password answer", new[] { "resetPassword" }) });
+ }
+ //ok, we should be able to reset it
+ try
+ {
+ var newPass = membershipProvider.ResetPassword(
+ username,
+ membershipProvider.RequiresQuestionAndAnswer ? passwordModel.Answer : null);
+
+ //return the generated pword
+ return Attempt.Succeed(new PasswordChangedModel { ResetPassword = newPass });
+ }
+ catch (Exception ex)
+ {
+ _logger.Warn("Could not reset member password", ex);
+ return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not reset password, error: " + ex.Message + " (see log for full details)", new[] { "resetPassword" }) });
+ }
+ }
+
+ //we're not resetting it so we need to try to change it.
+
+ if (passwordModel.NewPassword.IsNullOrWhiteSpace())
+ {
+ return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Cannot set an empty password", new[] { "value" }) });
+ }
+
+ //This is an edge case and is only necessary for backwards compatibility:
+ if (membershipProvider is MembershipProviderBase umbracoBaseProvider && umbracoBaseProvider.AllowManuallyChangingPassword)
+ {
+ //this provider allows manually changing the password without the old password, so we can just do it
+ try
+ {
+ var result = umbracoBaseProvider.ChangePassword(username, "", passwordModel.NewPassword);
+ return result == false
+ ? Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, invalid username or password", new[] { "value" }) })
+ : Attempt.Succeed(new PasswordChangedModel());
+ }
+ catch (Exception ex)
+ {
+ _logger.Warn("Could not change member password", ex);
+ return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, error: " + ex.Message + " (see log for full details)", new[] { "value" }) });
+ }
+ }
+
+ //The provider does not support manually chaning the password but no old password supplied - need to return an error
+ if (passwordModel.OldPassword.IsNullOrWhiteSpace() && membershipProvider.EnablePasswordRetrieval == false)
+ {
+ //if password retrieval is not enabled but there is no old password we cannot continue
+ return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password cannot be changed without the old password", new[] { "oldPassword" }) });
+ }
+
+ if (passwordModel.OldPassword.IsNullOrWhiteSpace() == false)
+ {
+ //if an old password is suplied try to change it
+
+ try
+ {
+ var result = membershipProvider.ChangePassword(username, passwordModel.OldPassword, passwordModel.NewPassword);
+ return result == false
+ ? Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, invalid username or password", new[] { "oldPassword" }) })
+ : Attempt.Succeed(new PasswordChangedModel());
+ }
+ catch (Exception ex)
+ {
+ _logger.Warn("Could not change member password", ex);
+ return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, error: " + ex.Message + " (see log for full details)", new[] { "value" }) });
+ }
+ }
+
+ if (membershipProvider.EnablePasswordRetrieval == false)
+ {
+ //we cannot continue if we cannot get the current password
+ return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password cannot be changed without the old password", new[] { "oldPassword" }) });
+ }
+ if (membershipProvider.RequiresQuestionAndAnswer && passwordModel.Answer.IsNullOrWhiteSpace())
+ {
+ //if the question answer is required but there isn't one, we cannot continue
+ return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password cannot be changed without the password answer", new[] { "value" }) });
+ }
+
+ //lets try to get the old one so we can change it
+ try
+ {
+ var oldPassword = membershipProvider.GetPassword(
+ username,
+ membershipProvider.RequiresQuestionAndAnswer ? passwordModel.Answer : null);
+
+ try
+ {
+ var result = membershipProvider.ChangePassword(username, oldPassword, passwordModel.NewPassword);
+ return result == false
+ ? Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password", new[] { "value" }) })
+ : Attempt.Succeed(new PasswordChangedModel());
+ }
+ catch (Exception ex1)
+ {
+ _logger.Warn("Could not change member password", ex1);
+ return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, error: " + ex1.Message + " (see log for full details)", new[] { "value" }) });
+ }
+
+ }
+ catch (Exception ex2)
+ {
+ _logger.Warn("Could not retrieve member password", ex2);
+ return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, error: " + ex2.Message + " (see log for full details)", new[] { "value" }) });
+ }
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/Editors/RedirectUrlManagementController.cs b/src/Umbraco.Web/Editors/RedirectUrlManagementController.cs
index bd5ddb5062..d63eaa2b48 100644
--- a/src/Umbraco.Web/Editors/RedirectUrlManagementController.cs
+++ b/src/Umbraco.Web/Editors/RedirectUrlManagementController.cs
@@ -78,7 +78,7 @@ namespace Umbraco.Web.Editors
var userIsAdmin = Umbraco.UmbracoContext.Security.CurrentUser.IsAdmin();
if (userIsAdmin == false)
{
- var errorMessage = $"User of type {Umbraco.UmbracoContext.Security.CurrentUser.UserType.Alias} is not allowed to toggle the URL tracker.";
+ var errorMessage = "User is not a member of the administrators group and so is not allowed to toggle the URL tracker";
_logger.Debug(errorMessage);
throw new SecurityException(errorMessage);
}
diff --git a/src/Umbraco.Web/Editors/SectionController.cs b/src/Umbraco.Web/Editors/SectionController.cs
index c414b41fdf..d527724abf 100644
--- a/src/Umbraco.Web/Editors/SectionController.cs
+++ b/src/Umbraco.Web/Editors/SectionController.cs
@@ -17,5 +17,12 @@ namespace Umbraco.Web.Editors
var sections = Services.SectionService.GetAllowedSections(Security.GetUserId());
return sections.Select(Mapper.Map);
}
+
+ public IEnumerable GetAllSections()
+ {
+ var sections = Services.SectionService.GetSections();
+ return sections.Select(Mapper.Map);
+ }
+
}
}
diff --git a/src/Umbraco.Web/Editors/UserController.cs b/src/Umbraco.Web/Editors/UserController.cs
deleted file mode 100644
index 4454709227..0000000000
--- a/src/Umbraco.Web/Editors/UserController.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-using System.Net;
-using System.Web.Http;
-using Umbraco.Web.Mvc;
-using Umbraco.Web.WebApi.Filters;
-using Constants = Umbraco.Core.Constants;
-
-namespace Umbraco.Web.Editors
-{
- [PluginController("UmbracoApi")]
- [UmbracoApplicationAuthorize(Constants.Applications.Users)]
- public class UserController : UmbracoAuthorizedJsonController
- {
- ///
- /// Disables the user with the given user id
- ///
- ///
- public bool PostDisableUser([FromUri]int userId)
- {
- var user = Services.UserService.GetUserById(userId);
- if (user == null)
- {
- throw new HttpResponseException(HttpStatusCode.NotFound);
- }
- //without the permanent flag, this will just disable
- Services.UserService.Delete(user);
- return true;
- }
- }
-}
diff --git a/src/Umbraco.Web/Editors/UserGroupValidateAttribute.cs b/src/Umbraco.Web/Editors/UserGroupValidateAttribute.cs
new file mode 100644
index 0000000000..35cf3930ab
--- /dev/null
+++ b/src/Umbraco.Web/Editors/UserGroupValidateAttribute.cs
@@ -0,0 +1,81 @@
+using System;
+using System.Net;
+using System.Net.Http;
+using System.Web.Http.Controllers;
+using System.Web.Http.Filters;
+using AutoMapper;
+using Umbraco.Core.Composing;
+using Umbraco.Core.Models.Membership;
+using Umbraco.Core.Services;
+using Umbraco.Web.Models.ContentEditing;
+using Umbraco.Web.WebApi;
+
+namespace Umbraco.Web.Editors
+{
+ internal sealed class UserGroupValidateAttribute : ActionFilterAttribute
+ {
+ private readonly IUserService _userService;
+
+ public UserGroupValidateAttribute()
+ {
+ }
+
+ public UserGroupValidateAttribute(IUserService userService)
+ {
+ if (_userService == null) throw new ArgumentNullException(nameof(userService));
+ _userService = userService;
+ }
+
+ private IUserService UserService => _userService ?? Current.Services.UserService; // fixme inject
+
+ public override void OnActionExecuting(HttpActionContext actionContext)
+ {
+ var userGroupSave = (UserGroupSave) actionContext.ActionArguments["userGroupSave"];
+
+ userGroupSave.Name = userGroupSave.Name.CleanForXss('[', ']', '(', ')', ':');
+ userGroupSave.Alias = userGroupSave.Alias.CleanForXss('[', ']', '(', ')', ':');
+
+ //Validate the usergroup exists or create one if required
+ IUserGroup persisted;
+ switch (userGroupSave.Action)
+ {
+ case ContentSaveAction.Save:
+ persisted = UserService.GetUserGroupById(Convert.ToInt32(userGroupSave.Id));
+ if (persisted == null)
+ {
+ var message = $"User group with id: {userGroupSave.Id} was not found";
+ actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.NotFound, message);
+ return;
+ }
+ //map the model to the persisted instance
+ Mapper.Map(userGroupSave, persisted);
+ break;
+ case ContentSaveAction.SaveNew:
+ //create the persisted model from mapping the saved model
+ persisted = Mapper.Map(userGroupSave);
+ ((UserGroup)persisted).ResetIdentity();
+ break;
+ default:
+ actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.NotFound, new ArgumentOutOfRangeException());
+ return;
+ }
+
+ //now assign the persisted entity to the model so we can use it in the action
+ userGroupSave.PersistedUserGroup = persisted;
+
+ var existing = UserService.GetUserGroupByAlias(userGroupSave.Alias);
+ if (existing != null && existing.Id != userGroupSave.PersistedUserGroup.Id)
+ {
+ actionContext.ModelState.AddModelError("Alias", "A user group with this alias already exists");
+ }
+
+ //TODO: Validate the name is unique?
+
+ if (actionContext.ModelState.IsValid == false)
+ {
+ //if it is not valid, do not continue and return the model state
+ actionContext.Response = actionContext.Request.CreateValidationErrorResponse(actionContext.ModelState);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/Editors/UserGroupsController.cs b/src/Umbraco.Web/Editors/UserGroupsController.cs
new file mode 100644
index 0000000000..ad896fe500
--- /dev/null
+++ b/src/Umbraco.Web/Editors/UserGroupsController.cs
@@ -0,0 +1,106 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Web.Http;
+using AutoMapper;
+using Umbraco.Core.Models.Membership;
+using Umbraco.Core.Services;
+using Umbraco.Web.Models.ContentEditing;
+using Umbraco.Web.Mvc;
+using Umbraco.Web.WebApi;
+using Umbraco.Web.WebApi.Filters;
+using Constants = Umbraco.Core.Constants;
+
+namespace Umbraco.Web.Editors
+{
+ [PluginController("UmbracoApi")]
+ [UmbracoApplicationAuthorize(Constants.Applications.Users)]
+ [PrefixlessBodyModelValidator]
+ public class UserGroupsController : UmbracoAuthorizedJsonController
+ {
+ [UserGroupValidate]
+ public UserGroupDisplay PostSaveUserGroup(UserGroupSave userGroupSave)
+ {
+ if (userGroupSave == null) throw new ArgumentNullException(nameof(userGroupSave));
+
+ //save the group
+ Services.UserService.Save(userGroupSave.PersistedUserGroup, userGroupSave.Users.ToArray());
+
+ //deal with permissions
+
+ //remove ones that have been removed
+ var existing = Services.UserService.GetPermissions(userGroupSave.PersistedUserGroup, true)
+ .ToDictionary(x => x.EntityId, x => x);
+ var toRemove = existing.Keys.Except(userGroupSave.AssignedPermissions.Select(x => x.Key));
+ foreach (var contentId in toRemove)
+ {
+ Services.UserService.RemoveUserGroupPermissions(userGroupSave.PersistedUserGroup.Id, contentId);
+ }
+
+ //update existing
+ foreach (var assignedPermission in userGroupSave.AssignedPermissions)
+ {
+ Services.UserService.ReplaceUserGroupPermissions(
+ userGroupSave.PersistedUserGroup.Id,
+ assignedPermission.Value.Select(x => x[0]),
+ assignedPermission.Key);
+ }
+
+ var display = Mapper.Map(userGroupSave.PersistedUserGroup);
+
+ display.AddSuccessNotification(Services.TextService.Localize("speechBubbles/operationSavedHeader"), Services.TextService.Localize("speechBubbles/editUserGroupSaved"));
+ return display;
+ }
+
+ ///
+ /// Returns the scaffold for creating a new user group
+ ///
+ ///
+ public UserGroupDisplay GetEmptyUserGroup()
+ {
+ return Mapper.Map(new UserGroup());
+ }
+
+ ///
+ /// Returns all user groups
+ ///
+ ///
+ public IEnumerable GetUserGroups()
+ {
+ return Mapper.Map, IEnumerable>(Services.UserService.GetAllUserGroups());
+ }
+
+ ///
+ /// Return a user group
+ ///
+ ///
+ public UserGroupDisplay GetUserGroup(int id)
+ {
+ var found = Services.UserService.GetUserGroupById(id);
+ if (found == null)
+ throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound));
+
+ var display = Mapper.Map(found);
+
+ return display;
+ }
+
+ [HttpPost]
+ [HttpDelete]
+ public HttpResponseMessage PostDeleteUserGroups([FromUri] int[] userGroupIds)
+ {
+ var userGroups = Services.UserService.GetAllUserGroups(userGroupIds).ToArray();
+ foreach (var userGroup in userGroups)
+ {
+ Services.UserService.DeleteUserGroup(userGroup);
+ }
+ if (userGroups.Length > 1)
+ return Request.CreateNotificationSuccessResponse(
+ Services.TextService.Localize("speechBubbles/deleteUserGroupsSuccess", new[] {userGroups.Length.ToString()}));
+ return Request.CreateNotificationSuccessResponse(
+ Services.TextService.Localize("speechBubbles/deleteUserGroupSuccess", new[] {userGroups[0].Name}));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs
new file mode 100644
index 0000000000..236c669db4
--- /dev/null
+++ b/src/Umbraco.Web/Editors/UsersController.cs
@@ -0,0 +1,593 @@
+using System;
+using System.Collections.Generic;
+using System.Configuration;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Runtime.Serialization;
+using System.Threading.Tasks;
+using System.Web;
+using System.Web.Http;
+using System.Web.Mvc;
+using System.Web.Routing;
+using System.Web.Security;
+using System.Web.WebPages;
+using AutoMapper;
+using ClientDependency.Core;
+using Microsoft.AspNet.Identity;
+using Umbraco.Core;
+using Umbraco.Core.Cache;
+using Umbraco.Core.Configuration;
+using Umbraco.Core.IO;
+using Umbraco.Core.Models;
+using Umbraco.Core.Models.Identity;
+using Umbraco.Core.Models.Membership;
+using Umbraco.Core.Persistence.DatabaseModelDefinitions;
+using Umbraco.Core.Security;
+using Umbraco.Core.Services;
+using Umbraco.Web.Models.ContentEditing;
+using Umbraco.Web.Mvc;
+using Umbraco.Web.WebApi;
+using Umbraco.Web.WebApi.Filters;
+using Constants = Umbraco.Core.Constants;
+using IUser = Umbraco.Core.Models.Membership.IUser;
+using Task = System.Threading.Tasks.Task;
+
+namespace Umbraco.Web.Editors
+{
+ [PluginController("UmbracoApi")]
+ [UmbracoApplicationAuthorize(Constants.Applications.Users)]
+ [PrefixlessBodyModelValidator]
+ [IsCurrentUserModelFilter]
+ public class UsersController : UmbracoAuthorizedJsonController
+ {
+ ///
+ /// Constructor
+ ///
+ public UsersController()
+ : this(UmbracoContext.Current)
+ {
+ }
+
+ ///
+ /// Constructor
+ ///
+ ///
+ public UsersController(UmbracoContext umbracoContext)
+ : base(umbracoContext)
+ {
+ }
+
+ public UsersController(UmbracoContext umbracoContext, UmbracoHelper umbracoHelper, BackOfficeUserManager backOfficeUserManager)
+ : base(umbracoContext, umbracoHelper, backOfficeUserManager)
+ {
+ }
+
+
+ ///
+ /// Returns a list of the sizes of gravatar urls for the user or null if the gravatar server cannot be reached
+ ///
+ ///
+ public string[] GetCurrentUserAvatarUrls()
+ {
+ var urls = UmbracoContext.Security.CurrentUser.GetCurrentUserAvatarUrls(Services.UserService, ApplicationContext.ApplicationCache.StaticCache);
+ if (urls == null)
+ throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, "Could not access Gravatar endpoint"));
+
+ return urls;
+ }
+
+ [AppendUserModifiedHeader("id")]
+ [FileUploadCleanupFilter(false)]
+ public async Task PostSetAvatar(int id)
+ {
+ return await PostSetAvatarInternal(Request, Services.UserService, ApplicationContext.ApplicationCache.StaticCache, id);
+ }
+
+ internal static async Task PostSetAvatarInternal(HttpRequestMessage request, IUserService userService, ICacheProvider staticCache, int id)
+ {
+ if (request.Content.IsMimeMultipartContent() == false)
+ {
+ throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
+ }
+
+ var root = IOHelper.MapPath("~/App_Data/TEMP/FileUploads");
+ //ensure it exists
+ Directory.CreateDirectory(root);
+ var provider = new MultipartFormDataStreamProvider(root);
+
+ var result = await request.Content.ReadAsMultipartAsync(provider);
+
+ //must have a file
+ if (result.FileData.Count == 0)
+ {
+ return request.CreateResponse(HttpStatusCode.NotFound);
+ }
+
+ var user = userService.GetUserById(id);
+ if (user == null)
+ return request.CreateResponse(HttpStatusCode.NotFound);
+
+ var tempFiles = new PostedFiles();
+
+ if (result.FileData.Count > 1)
+ return request.CreateValidationErrorResponse("The request was not formatted correctly, only one file can be attached to the request");
+
+ //get the file info
+ var file = result.FileData[0];
+ var fileName = file.Headers.ContentDisposition.FileName.Trim(new[] { '\"' }).TrimEnd();
+ var safeFileName = fileName.ToSafeFileName();
+ var ext = safeFileName.Substring(safeFileName.LastIndexOf('.') + 1).ToLower();
+
+ if (UmbracoConfig.For.UmbracoSettings().Content.DisallowedUploadFiles.Contains(ext) == false)
+ {
+ //generate a path of known data, we don't want this path to be guessable
+ user.Avatar = "UserAvatars/" + (user.Id + safeFileName).ToSHA1() + "." + ext;
+
+ using (var fs = System.IO.File.OpenRead(file.LocalFileName))
+ {
+ FileSystemProviderManager.Current.MediaFileSystem.AddFile(user.Avatar, fs, true);
+ }
+
+ userService.Save(user);
+
+ //track the temp file so the cleanup filter removes it
+ tempFiles.UploadedFiles.Add(new ContentItemFile
+ {
+ TempFilePath = file.LocalFileName
+ });
+ }
+
+ return request.CreateResponse(HttpStatusCode.OK, user.GetCurrentUserAvatarUrls(userService, staticCache));
+ }
+
+ [AppendUserModifiedHeader("id")]
+ public HttpResponseMessage PostClearAvatar(int id)
+ {
+ var found = Services.UserService.GetUserById(id);
+ if (found == null)
+ return Request.CreateResponse(HttpStatusCode.NotFound);
+
+ var filePath = found.Avatar;
+
+ //if the filePath is already null it will mean that the user doesn't have a custom avatar and their gravatar is currently
+ //being used (if they have one). This means they want to remove their gravatar too which we can do by setting a special value
+ //for the avatar.
+ if (filePath.IsNullOrWhiteSpace() == false)
+ {
+ found.Avatar = null;
+ }
+ else
+ {
+ //set a special value to indicate to not have any avatar
+ found.Avatar = "none";
+ }
+
+ Services.UserService.Save(found);
+
+ if (filePath.IsNullOrWhiteSpace() == false)
+ {
+ if (FileSystemProviderManager.Current.MediaFileSystem.FileExists(filePath))
+ FileSystemProviderManager.Current.MediaFileSystem.DeleteFile(filePath);
+ }
+
+ return Request.CreateResponse(HttpStatusCode.OK, found.GetCurrentUserAvatarUrls(Services.UserService, ApplicationContext.ApplicationCache.StaticCache));
+ }
+
+ ///
+ /// Gets a user by Id
+ ///
+ ///
+ ///
+ public UserDisplay GetById(int id)
+ {
+ var user = Services.UserService.GetUserById(id);
+ if (user == null)
+ {
+ throw new HttpResponseException(HttpStatusCode.NotFound);
+ }
+ return Mapper.Map(user);
+ }
+
+
+
+ ///
+ /// Returns a paged users collection
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public PagedUserResult GetPagedUsers(
+ int pageNumber = 1,
+ int pageSize = 10,
+ string orderBy = "username",
+ Direction orderDirection = Direction.Ascending,
+ [FromUri]string[] userGroups = null,
+ [FromUri]UserState[] userStates = null,
+ string filter = "")
+ {
+ long pageIndex = pageNumber - 1;
+ long total;
+ var result = Services.UserService.GetAll(pageIndex, pageSize, out total, orderBy, orderDirection, userStates, userGroups, filter);
+
+ var paged = new PagedUserResult(total, pageNumber, pageSize)
+ {
+ Items = Mapper.Map>(result),
+ UserStates = Services.UserService.GetUserStates()
+ };
+
+ return paged;
+ }
+
+ ///
+ /// Creates a new user
+ ///
+ ///
+ ///
+ public async Task PostCreateUser(UserInvite userSave)
+ {
+ if (userSave == null) throw new ArgumentNullException("userSave");
+
+ if (ModelState.IsValid == false)
+ {
+ throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState));
+ }
+
+ var existing = Services.UserService.GetByEmail(userSave.Email);
+ if (existing != null)
+ {
+ ModelState.AddModelError("Email", "A user with the email already exists");
+ throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState));
+ }
+
+ //we want to create the user with the UserManager, this ensures the 'empty' (special) password
+ //format is applied without us having to duplicate that logic
+ var identityUser = BackOfficeIdentityUser.CreateNew(userSave.Email, userSave.Email, GlobalSettings.DefaultUILanguage);
+ identityUser.Name = userSave.Name;
+
+ var created = await UserManager.CreateAsync(identityUser);
+ if (created.Succeeded == false)
+ {
+ throw new HttpResponseException(
+ Request.CreateNotificationValidationErrorResponse(string.Join(", ", created.Errors)));
+ }
+
+ //we need to generate a password, however we can only do that if the user manager has a password validator that
+ //we can read values from
+ var passwordValidator = UserManager.PasswordValidator as PasswordValidator;
+ var resetPassword = string.Empty;
+ if (passwordValidator != null)
+ {
+ var password = UserManager.GeneratePassword();
+
+ var result = await UserManager.AddPasswordAsync(identityUser.Id, password);
+ if (result.Succeeded == false)
+ {
+ throw new HttpResponseException(
+ Request.CreateNotificationValidationErrorResponse(string.Join(", ", created.Errors)));
+ }
+ resetPassword = password;
+ }
+
+ //now re-look the user back up which will now exist
+ var user = Services.UserService.GetByEmail(userSave.Email);
+
+ //map the save info over onto the user
+ user = Mapper.Map(userSave, user);
+
+ //since the back office user is creating this user, they will be set to approved
+ user.IsApproved = true;
+
+ Services.UserService.Save(user);
+
+ var display = Mapper.Map(user);
+ display.ResetPasswordValue = resetPassword;
+ return display;
+ }
+
+ ///
+ /// Invites a user
+ ///
+ ///
+ ///
+ ///
+ /// This will email the user an invite and generate a token that will be validated in the email
+ ///
+ public async Task PostInviteUser(UserInvite userSave)
+ {
+ if (userSave == null) throw new ArgumentNullException("userSave");
+
+ if (userSave.Message.IsNullOrWhiteSpace())
+ ModelState.AddModelError("Message", "Message cannot be empty");
+
+ if (ModelState.IsValid == false)
+ {
+ throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState));
+ }
+
+ var hasSmtp = GlobalSettings.HasSmtpServerConfigured(RequestContext.VirtualPathRoot);
+ if (hasSmtp == false)
+ {
+ throw new HttpResponseException(
+ Request.CreateNotificationValidationErrorResponse("No Email server is configured"));
+ }
+
+ var user = Services.UserService.GetByEmail(userSave.Email);
+ if (user != null && (user.LastLoginDate != default(DateTime) || user.EmailConfirmedDate.HasValue))
+ {
+ ModelState.AddModelError("Email", "A user with the email already exists");
+ throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState));
+ }
+
+ if (user == null)
+ {
+ //we want to create the user with the UserManager, this ensures the 'empty' (special) password
+ //format is applied without us having to duplicate that logic
+ var identityUser = BackOfficeIdentityUser.CreateNew(userSave.Email, userSave.Email, GlobalSettings.DefaultUILanguage);
+ identityUser.Name = userSave.Name;
+
+ var created = await UserManager.CreateAsync(identityUser);
+ if (created.Succeeded == false)
+ {
+ throw new HttpResponseException(
+ Request.CreateNotificationValidationErrorResponse(string.Join(", ", created.Errors)));
+ }
+
+ //now re-look the user back up
+ user = Services.UserService.GetByEmail(userSave.Email);
+ }
+
+ //map the save info over onto the user
+ user = Mapper.Map(userSave, user);
+
+ //ensure the invited date is set
+ user.InvitedDate = DateTime.Now;
+
+ //Save the updated user
+ Services.UserService.Save(user);
+ var display = Mapper.Map(user);
+
+ //send the email
+
+ await SendUserInviteEmailAsync(display, Security.CurrentUser.Name, user, userSave.Message);
+
+ return display;
+ }
+
+
+
+ private HttpContextBase EnsureHttpContext()
+ {
+ var attempt = this.TryGetHttpContext();
+ if (attempt.Success == false)
+ throw new InvalidOperationException("This method requires that an HttpContext be active");
+ return attempt.Result;
+ }
+
+ private async Task SendUserInviteEmailAsync(UserBasic userDisplay, string from, IUser to, string message)
+ {
+ var token = await UserManager.GenerateEmailConfirmationTokenAsync((int)userDisplay.Id);
+
+ var inviteToken = string.Format("{0}{1}{2}",
+ (int)userDisplay.Id,
+ WebUtility.UrlEncode("|"),
+ token.ToUrlBase64());
+
+ // Get an mvc helper to get the url
+ var http = EnsureHttpContext();
+ var urlHelper = new UrlHelper(http.Request.RequestContext);
+ var action = urlHelper.Action("VerifyInvite", "BackOffice",
+ new
+ {
+ area = GlobalSettings.UmbracoMvcArea,
+ invite = inviteToken
+ });
+
+ // Construct full URL using configured application URL (which will fall back to request)
+ var applicationUri = new Uri(ApplicationContext.UmbracoApplicationUrl);
+ var inviteUri = new Uri(applicationUri, action);
+
+ var emailSubject = Services.TextService.Localize("user/inviteEmailCopySubject",
+ //Ensure the culture of the found user is used for the email!
+ UserExtensions.GetUserCulture(to.Language, Services.TextService));
+ 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() });
+
+ await UserManager.EmailService.SendAsync(new IdentityMessage
+ {
+ Body = emailBody,
+ Destination = userDisplay.Email,
+ Subject = emailSubject
+ });
+
+ }
+
+ ///
+ /// Saves a user
+ ///
+ ///
+ ///
+ public async Task PostSaveUser(UserSave userSave)
+ {
+ if (userSave == null) throw new ArgumentNullException("userSave");
+
+ if (ModelState.IsValid == false)
+ {
+ throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState));
+ }
+
+ var intId = userSave.Id.TryConvertTo();
+ if (intId.Success == false)
+ throw new HttpResponseException(HttpStatusCode.NotFound);
+
+ var found = Services.UserService.GetUserById(intId.Result);
+ if (found == null)
+ throw new HttpResponseException(HttpStatusCode.NotFound);
+
+ var hasErrors = false;
+
+ var existing = Services.UserService.GetByEmail(userSave.Email);
+ if (existing != null && existing.Id != userSave.Id)
+ {
+ ModelState.AddModelError("Email", "A user with the email already exists");
+ hasErrors = true;
+ }
+ existing = Services.UserService.GetByUsername(userSave.Username);
+ if (existing != null && existing.Id != userSave.Id)
+ {
+ ModelState.AddModelError("Username", "A user with the username already exists");
+ hasErrors = true;
+ }
+ // going forward we prefer to align usernames with email, so we should cross-check to make sure
+ // the email or username isn't somehow being used by anyone.
+ existing = Services.UserService.GetByEmail(userSave.Username);
+ if (existing != null && existing.Id != userSave.Id)
+ {
+ ModelState.AddModelError("Username", "A user using this as their email already exists");
+ hasErrors = true;
+ }
+ existing = Services.UserService.GetByUsername(userSave.Email);
+ if (existing != null && existing.Id != userSave.Id)
+ {
+ ModelState.AddModelError("Email", "A user using this as their username already exists");
+ hasErrors = true;
+ }
+
+ // 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)
+ {
+ userSave.Username = userSave.Email;
+ }
+
+ var resetPasswordValue = string.Empty;
+ if (userSave.ChangePassword != null)
+ {
+ var passwordChanger = new PasswordChanger(Logger, Services.UserService);
+
+ var passwordChangeResult = await passwordChanger.ChangePasswordWithIdentityAsync(found, userSave.ChangePassword, ModelState, UserManager);
+ if (passwordChangeResult.Success)
+ {
+ //depending on how the provider is configured, the password may be reset so let's store that for later
+ resetPasswordValue = passwordChangeResult.Result.ResetPassword;
+
+ //need to re-get the user
+ found = Services.UserService.GetUserById(intId.Result);
+ }
+ else
+ {
+ hasErrors = true;
+ }
+ }
+
+ if (hasErrors)
+ throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState));
+
+ //merge the save data onto the user
+ var user = Mapper.Map(userSave, found);
+
+ Services.UserService.Save(user);
+
+ var display = Mapper.Map(user);
+
+ //re-map the password reset value (if any)
+ if (resetPasswordValue.IsNullOrWhiteSpace() == false)
+ display.ResetPasswordValue = resetPasswordValue;
+
+ display.AddSuccessNotification(Services.TextService.Localize("speechBubbles/operationSavedHeader"), Services.TextService.Localize("speechBubbles/editUserSaved"));
+ return display;
+ }
+
+ ///
+ /// Disables the users with the given user ids
+ ///
+ ///
+ public HttpResponseMessage PostDisableUsers([FromUri]int[] userIds)
+ {
+ if (userIds.Contains(Security.GetUserId()))
+ {
+ throw new HttpResponseException(
+ Request.CreateNotificationValidationErrorResponse("The current user cannot disable itself"));
+ }
+
+ var users = Services.UserService.GetUsersById(userIds).ToArray();
+ foreach (var u in users)
+ {
+ u.IsApproved = false;
+ u.InvitedDate = null;
+ }
+ Services.UserService.Save(users);
+
+ if (users.Length > 1)
+ {
+ return Request.CreateNotificationSuccessResponse(
+ Services.TextService.Localize("speechBubbles/disableUsersSuccess", new[] {userIds.Length.ToString()}));
+ }
+
+ return Request.CreateNotificationSuccessResponse(
+ Services.TextService.Localize("speechBubbles/disableUserSuccess", new[] { users[0].Name }));
+ }
+
+ ///
+ /// Enables the users with the given user ids
+ ///
+ ///
+ public HttpResponseMessage PostEnableUsers([FromUri]int[] userIds)
+ {
+ var users = Services.UserService.GetUsersById(userIds).ToArray();
+ foreach (var u in users)
+ {
+ u.IsApproved = true;
+ }
+ Services.UserService.Save(users);
+
+ if (users.Length > 1)
+ {
+ return Request.CreateNotificationSuccessResponse(
+ Services.TextService.Localize("speechBubbles/enableUsersSuccess", new[] { userIds.Length.ToString() }));
+ }
+
+ return Request.CreateNotificationSuccessResponse(
+ Services.TextService.Localize("speechBubbles/enableUserSuccess", new[] { users[0].Name }));
+ }
+
+ public HttpResponseMessage PostSetUserGroupsOnUsers([FromUri]string[] userGroupAliases, [FromUri]int[] userIds)
+ {
+ var users = Services.UserService.GetUsersById(userIds).ToArray();
+ var userGroups = Services.UserService.GetUserGroupsByAlias(userGroupAliases).Select(x => x.ToReadOnlyGroup()).ToArray();
+ foreach (var u in users)
+ {
+ u.ClearGroups();
+ foreach (var userGroup in userGroups)
+ {
+ u.AddGroup(userGroup);
+ }
+ }
+ Services.UserService.Save(users);
+ return Request.CreateNotificationSuccessResponse(
+ Services.TextService.Localize("speechBubbles/setUserGroupOnUsersSuccess"));
+ }
+
+ public class PagedUserResult : PagedResult
+ {
+ public PagedUserResult(long totalItems, long pageNumber, long pageSize) : base(totalItems, pageNumber, pageSize)
+ {
+ UserStates = new Dictionary();
+ }
+
+ ///
+ /// This is basically facets of UserStates key = state, value = count
+ ///
+ [DataMember(Name = "userStates")]
+ public IDictionary UserStates { get; set; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/Macros/MacroRenderer.cs b/src/Umbraco.Web/Macros/MacroRenderer.cs
index 782cf8b3c4..7d60777b86 100644
--- a/src/Umbraco.Web/Macros/MacroRenderer.cs
+++ b/src/Umbraco.Web/Macros/MacroRenderer.cs
@@ -525,9 +525,11 @@ namespace Umbraco.Web.Macros
attributeValue = context?.Request.GetCookieValue(name);
break;
case '#':
+ if (pageElements == null) pageElements = GetPageElements();
attributeValue = pageElements[name]?.ToString();
break;
case '$':
+ if (pageElements == null) pageElements = GetPageElements();
attributeValue = pageElements[name]?.ToString();
if (string.IsNullOrEmpty(attributeValue))
attributeValue = ParseAttributeOnParents(pageElements, name);
@@ -561,6 +563,14 @@ namespace Umbraco.Web.Macros
return value;
}
+ private static IDictionary GetPageElements()
+ {
+ IDictionary pageElements = null;
+ if (HttpContext.Current.Items["pageElements"] != null)
+ pageElements = (IDictionary)HttpContext.Current.Items["pageElements"];
+ return pageElements;
+ }
+
#endregion
#region RTE macros
diff --git a/src/Umbraco.Web/Models/ContentEditing/AssignedContentPermissions.cs b/src/Umbraco.Web/Models/ContentEditing/AssignedContentPermissions.cs
new file mode 100644
index 0000000000..dc071052cc
--- /dev/null
+++ b/src/Umbraco.Web/Models/ContentEditing/AssignedContentPermissions.cs
@@ -0,0 +1,21 @@
+using System.Collections.Generic;
+using System.Runtime.Serialization;
+
+namespace Umbraco.Web.Models.ContentEditing
+{
+ ///
+ /// The permissions assigned to a content node
+ ///
+ ///
+ /// The underlying data such as Name, etc... is that of the Content item
+ ///
+ [DataContract(Name = "contentPermissions", Namespace = "")]
+ public class AssignedContentPermissions : EntityBasic
+ {
+ ///
+ /// The assigned permissions to the content item organized by permission group name
+ ///
+ [DataMember(Name = "permissions")]
+ public IDictionary> AssignedPermissions { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/Models/ContentEditing/AssignedUserGroupPermissions.cs b/src/Umbraco.Web/Models/ContentEditing/AssignedUserGroupPermissions.cs
new file mode 100644
index 0000000000..5428de883f
--- /dev/null
+++ b/src/Umbraco.Web/Models/ContentEditing/AssignedUserGroupPermissions.cs
@@ -0,0 +1,38 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.Serialization;
+
+namespace Umbraco.Web.Models.ContentEditing
+{
+ ///
+ /// The user group permissions assigned to a content node
+ ///
+ ///
+ /// The underlying data such as Name, etc... is that of the User Group
+ ///
+ [DataContract(Name = "userGroupPermissions", Namespace = "")]
+ public class AssignedUserGroupPermissions : EntityBasic
+ {
+ ///
+ /// The assigned permissions for the user group organized by permission group name
+ ///
+ [DataMember(Name = "permissions")]
+ public IDictionary> AssignedPermissions { get; set; }
+
+ ///
+ /// The default permissions for the user group organized by permission group name
+ ///
+ [DataMember(Name = "defaultPermissions")]
+ public IDictionary> DefaultPermissions { get; set; }
+
+ public static IDictionary> ClonePermissions(IDictionary> permissions)
+ {
+ var result = new Dictionary>();
+ foreach (var permission in permissions)
+ {
+ result[permission.Key] = new List(permission.Value.Select(x => (Permission)x.Clone()));
+ }
+ return result;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentItemBasic.cs b/src/Umbraco.Web/Models/ContentEditing/ContentItemBasic.cs
index d097065c88..a1d08eca2c 100644
--- a/src/Umbraco.Web/Models/ContentEditing/ContentItemBasic.cs
+++ b/src/Umbraco.Web/Models/ContentEditing/ContentItemBasic.cs
@@ -29,10 +29,10 @@ namespace Umbraco.Web.Models.ContentEditing
public bool HasPublishedVersion { get; set; }
[DataMember(Name = "owner")]
- public UserBasic Owner { get; set; }
+ public UserProfile Owner { get; set; }
[DataMember(Name = "updater")]
- public UserBasic Updater { get; set; }
+ public UserProfile Updater { get; set; }
[DataMember(Name = "contentTypeAlias", IsRequired = true)]
[Required(AllowEmptyStrings = false)]
@@ -89,7 +89,7 @@ namespace Umbraco.Web.Models.ContentEditing
///
/// This is not used for outgoing model information.
///
- [JsonIgnore]
+ [IgnoreDataMember]
internal TPersisted PersistedContent { get; set; }
///
@@ -100,7 +100,7 @@ namespace Umbraco.Web.Models.ContentEditing
/// instead of having to look up all the data individually.
/// This is not used for outgoing model information.
///
- [JsonIgnore]
+ [IgnoreDataMember]
internal ContentItemDto ContentDto { get; set; }
diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs
index 6aaed3fa64..4870bd1cc6 100644
--- a/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs
+++ b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs
@@ -56,7 +56,7 @@ namespace Umbraco.Web.Models.ContentEditing
/// Each char represents a button which we can then map on the front-end to the correct actions
///
[DataMember(Name = "allowedActions")]
- public IEnumerable AllowedActions { get; set; }
+ public IEnumerable AllowedActions { get; set; }
}
-}
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentTypeBasic.cs b/src/Umbraco.Web/Models/ContentEditing/ContentTypeBasic.cs
index a6a0caf67a..f1514afbac 100644
--- a/src/Umbraco.Web/Models/ContentEditing/ContentTypeBasic.cs
+++ b/src/Umbraco.Web/Models/ContentEditing/ContentTypeBasic.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Runtime.Serialization;
@@ -18,6 +19,11 @@ namespace Umbraco.Web.Models.ContentEditing
[DataContract(Name = "contentType", Namespace = "")]
public class ContentTypeBasic : EntityBasic
{
+ public ContentTypeBasic()
+ {
+ Blueprints = new Dictionary();
+ }
+
///
/// Overridden to apply our own validation attributes since this is not always required for other classes
///
@@ -105,5 +111,9 @@ namespace Umbraco.Web.Models.ContentEditing
: IOHelper.ResolveUrl("~/umbraco/images/thumbnails/" + Thumbnail);
}
}
+
+ [DataMember(Name = "blueprints")]
+ [ReadOnly(true)]
+ public IDictionary Blueprints { get; set; }
}
}
diff --git a/src/Umbraco.Web/Models/ContentEditing/DataTypeSave.cs b/src/Umbraco.Web/Models/ContentEditing/DataTypeSave.cs
index 7e003175ab..1045d1227a 100644
--- a/src/Umbraco.Web/Models/ContentEditing/DataTypeSave.cs
+++ b/src/Umbraco.Web/Models/ContentEditing/DataTypeSave.cs
@@ -31,13 +31,13 @@ namespace Umbraco.Web.Models.ContentEditing
///
/// The real persisted data type
///
- [JsonIgnore]
+ [IgnoreDataMember]
internal IDataTypeDefinition PersistedDataType { get; set; }
///
/// The PropertyEditor assigned
///
- [JsonIgnore]
+ [IgnoreDataMember]
internal PropertyEditor PropertyEditor { get; set; }
}
diff --git a/src/Umbraco.Web/Models/ContentEditing/EntityTypeSearchResult.cs b/src/Umbraco.Web/Models/ContentEditing/EntityTypeSearchResult.cs
deleted file mode 100644
index 27ddb18406..0000000000
--- a/src/Umbraco.Web/Models/ContentEditing/EntityTypeSearchResult.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using System.Collections.Generic;
-using System.Runtime.Serialization;
-using Examine;
-
-namespace Umbraco.Web.Models.ContentEditing
-{
- ///
- /// Represents a search result by entity type
- ///
- [DataContract(Name = "searchResult", Namespace = "")]
- public class EntityTypeSearchResult
- {
- [DataMember(Name = "type")]
- public string EntityType { get; set; }
-
- [DataMember(Name = "results")]
- public IEnumerable Results { get; set; }
- }
-}
diff --git a/src/Umbraco.Web/Models/ContentEditing/Permission.cs b/src/Umbraco.Web/Models/ContentEditing/Permission.cs
new file mode 100644
index 0000000000..9c40dc0aa6
--- /dev/null
+++ b/src/Umbraco.Web/Models/ContentEditing/Permission.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Runtime.Serialization;
+
+namespace Umbraco.Web.Models.ContentEditing
+{
+ [DataContract(Name = "permission", Namespace = "")]
+ public class Permission : ICloneable
+ {
+ [DataMember(Name = "name")]
+ public string Name { get; set; }
+
+ [DataMember(Name = "description")]
+ public string Description { get; set; }
+
+ [DataMember(Name = "checked")]
+ public bool Checked { get; set; }
+
+ [DataMember(Name = "icon")]
+ public string Icon { get; set; }
+
+ ///
+ /// We'll use this to map the categories but it wont' be returned in the json
+ ///
+ [IgnoreDataMember]
+ internal string Category { get; set; }
+
+ ///
+ /// The letter from the IAction
+ ///
+ [DataMember(Name = "permissionCode")]
+ public string PermissionCode { get; set; }
+
+ public object Clone()
+ {
+ return this.MemberwiseClone();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/Models/ContentEditing/SearchResultItem.cs b/src/Umbraco.Web/Models/ContentEditing/SearchResultItem.cs
index d55e80b1f9..f7062e679f 100644
--- a/src/Umbraco.Web/Models/ContentEditing/SearchResultItem.cs
+++ b/src/Umbraco.Web/Models/ContentEditing/SearchResultItem.cs
@@ -1,25 +1,15 @@
-namespace Umbraco.Web.Models.ContentEditing
+using System.Runtime.Serialization;
+
+namespace Umbraco.Web.Models.ContentEditing
{
- public class SearchResultItem
+ [DataContract(Name = "searchResult", Namespace = "")]
+ public class SearchResultItem : EntityBasic
{
///
- /// The string representation of the ID, used for Web responses
+ /// The score of the search result
///
- public string Id { get; set; }
-
- ///
- /// The name/title of the search result item
- ///
- public string Title { get; set; }
-
- ///
- /// The rank of the search result
- ///
- public int Rank { get; set; }
-
- ///
- /// Description/Synopsis of the item
- ///
- public string Description { get; set; }
+ [DataMember(Name = "score")]
+ public float Score { get; set; }
+
}
}
diff --git a/src/Umbraco.Web/Models/ContentEditing/TabbedContentItem.cs b/src/Umbraco.Web/Models/ContentEditing/TabbedContentItem.cs
index c4575a78ca..8e523574c2 100644
--- a/src/Umbraco.Web/Models/ContentEditing/TabbedContentItem.cs
+++ b/src/Umbraco.Web/Models/ContentEditing/TabbedContentItem.cs
@@ -29,7 +29,7 @@ namespace Umbraco.Web.Models.ContentEditing
///
/// This property cannot be set
///
- [JsonIgnore]
+ [IgnoreDataMember]
public override IEnumerable Properties
{
get { return Tabs.SelectMany(x => x.Properties); }
diff --git a/src/Umbraco.Web/Models/ContentEditing/TemplateDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/TemplateDisplay.cs
index 91c1aefdb0..ffa4e4e100 100644
--- a/src/Umbraco.Web/Models/ContentEditing/TemplateDisplay.cs
+++ b/src/Umbraco.Web/Models/ContentEditing/TemplateDisplay.cs
@@ -1,11 +1,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
-using System.Linq;
using System.Runtime.Serialization;
-using System.Text;
-using System.Threading.Tasks;
-using Umbraco.Core.Models.Validation;
namespace Umbraco.Web.Models.ContentEditing
{
@@ -24,6 +20,9 @@ namespace Umbraco.Web.Models.ContentEditing
[DataMember(Name = "alias")]
public string Alias { get; set; }
+ [DataMember(Name = "key")]
+ public Guid Key { get; set; }
+
[DataMember(Name = "content")]
public string Content { get; set; }
diff --git a/src/Umbraco.Web/Models/ContentEditing/TreeSearchResult.cs b/src/Umbraco.Web/Models/ContentEditing/TreeSearchResult.cs
new file mode 100644
index 0000000000..1da9d61c98
--- /dev/null
+++ b/src/Umbraco.Web/Models/ContentEditing/TreeSearchResult.cs
@@ -0,0 +1,35 @@
+using System.Collections.Generic;
+using System.Runtime.Serialization;
+using Examine;
+
+namespace Umbraco.Web.Models.ContentEditing
+{
+ ///
+ /// Represents a search result by entity type
+ ///
+ [DataContract(Name = "searchResult", Namespace = "")]
+ public class TreeSearchResult
+ {
+ [DataMember(Name = "appAlias")]
+ public string AppAlias { get; set; }
+
+ [DataMember(Name = "treeAlias")]
+ public string TreeAlias { get; set; }
+
+ ///
+ /// This is optional but if specified should be the name of an angular service to format the search result.
+ ///
+ [DataMember(Name = "jsSvc")]
+ public string JsFormatterService { get; set; }
+
+ ///
+ /// This is optional but if specified should be the name of a method on the jsSvc angular service to use, if not
+ /// specfied than it will expect the method to be called `format(searchResult, appAlias, treeAlias)`
+ ///
+ [DataMember(Name = "jsMethod")]
+ public string JsFormatterMethod { get; set; }
+
+ [DataMember(Name = "results")]
+ public IEnumerable Results { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/Models/ContentEditing/UserBasic.cs b/src/Umbraco.Web/Models/ContentEditing/UserBasic.cs
index 19da210840..581df571ab 100644
--- a/src/Umbraco.Web/Models/ContentEditing/UserBasic.cs
+++ b/src/Umbraco.Web/Models/ContentEditing/UserBasic.cs
@@ -1,27 +1,68 @@
-using System.ComponentModel.DataAnnotations;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
using System.Runtime.Serialization;
using Umbraco.Core.Models.Membership;
namespace Umbraco.Web.Models.ContentEditing
{
///
- /// A basic structure the represents a user
+ /// The user model used for paging and listing users in the UI
///
[DataContract(Name = "user", Namespace = "")]
- public class UserBasic : System.IComparable
+ [ReadOnly(true)]
+ public class UserBasic : EntityBasic, INotificationModel
{
- [DataMember(Name = "id", IsRequired = true)]
- [Required]
- public int UserId { get; set; }
-
- [DataMember(Name = "name", IsRequired = true)]
- [Required]
- public string Name { get; set; }
-
-
- int System.IComparable.CompareTo(object obj)
+ public UserBasic()
{
- return Name.CompareTo(((UserBasic)obj).Name);
- }
+ Notifications = new List();
+ UserGroups = new List();
+ }
+
+ [DataMember(Name = "username")]
+ public string Username { get; set; }
+
+ ///
+ /// The MD5 lowercase hash of the email which can be used by gravatar
+ ///
+ [DataMember(Name = "emailHash")]
+ public string EmailHash { get; set; }
+
+ [DataMember(Name = "lastLoginDate")]
+ public DateTime? LastLoginDate { get; set; }
+
+ ///
+ /// Returns a list of different size avatars
+ ///
+ [DataMember(Name = "avatars")]
+ public string[] Avatars { get; set; }
+
+ [DataMember(Name = "userState")]
+ public UserState UserState { get; set; }
+
+ [DataMember(Name = "culture", IsRequired = true)]
+ public string Culture { get; set; }
+
+ [DataMember(Name = "email", IsRequired = true)]
+ public string Email { get; set; }
+
+ ///
+ /// The list of group aliases assigned to the user
+ ///
+ [DataMember(Name = "userGroups")]
+ public IEnumerable UserGroups { get; set; }
+
+ ///
+ /// This is an info flag to denote if this object is the equivalent of the currently logged in user
+ ///
+ [DataMember(Name = "isCurrentUser")]
+ [ReadOnly(true)]
+ public bool IsCurrentUser { get; set; }
+
+ ///
+ /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes.
+ ///
+ [DataMember(Name = "notifications")]
+ public List Notifications { get; private set; }
}
}
diff --git a/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs b/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs
index 56f54832d6..e29b58fd6f 100644
--- a/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs
+++ b/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs
@@ -1,11 +1,16 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Runtime.Serialization;
namespace Umbraco.Web.Models.ContentEditing
-{
+{
+ ///
+ /// Represents information for the current user
+ ///
[DataContract(Name = "user", Namespace = "")]
- public class UserDetail : UserBasic
+ public class UserDetail : UserProfile
{
[DataMember(Name = "email", IsRequired = true)]
[Required]
@@ -21,21 +26,35 @@ namespace Umbraco.Web.Models.ContentEditing
[DataMember(Name = "emailHash")]
public string EmailHash { get; set; }
- [DataMember(Name = "userType", IsRequired = true)]
- [Required]
- public string UserType { get; set; }
+ [Obsolete("This should not be used it exists for legacy reasons only, use user groups instead, it will be removed in future versions")]
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ [ReadOnly(true)]
+ [DataMember(Name = "userType")]
+ public string UserType { get; set; }
///
/// Gets/sets the number of seconds for the user's auth ticket to expire
///
[DataMember(Name = "remainingAuthSeconds")]
public double SecondsUntilTimeout { get; set; }
+
+ ///
+ /// The user's calculated start nodes based on the start nodes they have assigned directly to them and via the groups they're assigned to
+ ///
+ [DataMember(Name = "startContentIds")]
+ public int[] StartContentIds { get; set; }
+
+ ///
+ /// The user's calculated start nodes based on the start nodes they have assigned directly to them and via the groups they're assigned to
+ ///
+ [DataMember(Name = "startMediaIds")]
+ public int[] StartMediaIds { get; set; }
- [DataMember(Name = "startContentId")]
- public int StartContentId { get; set; }
-
- [DataMember(Name = "startMediaId")]
- public int StartMediaId { get; set; }
+ ///
+ /// Returns a list of different size avatars
+ ///
+ [DataMember(Name = "avatars")]
+ public string[] Avatars { get; set; }
///
/// A list of sections the user is allowed to view.
diff --git a/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs
new file mode 100644
index 0000000000..4cff43e3b8
--- /dev/null
+++ b/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs
@@ -0,0 +1,43 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Globalization;
+using System.Runtime.Serialization;
+
+namespace Umbraco.Web.Models.ContentEditing
+{
+ ///
+ /// Represents a user that is being edited
+ ///
+ [DataContract(Name = "user", Namespace = "")]
+ [ReadOnly(true)]
+ public class UserDisplay : UserBasic
+ {
+ public UserDisplay()
+ {
+ AvailableCultures = new Dictionary();
+ StartContentIds = new List();
+ StartMediaIds = new List();
+ }
+
+ ///
+ /// Gets the available cultures (i.e. to populate a drop down)
+ /// The key is the culture stored in the database, the value is the Name
+ ///
+ [DataMember(Name = "availableCultures")]
+ public IDictionary AvailableCultures { get; set; }
+
+ [DataMember(Name = "startContentIds")]
+ public IEnumerable StartContentIds { get; set; }
+
+ [DataMember(Name = "startMediaIds")]
+ public IEnumerable StartMediaIds { get; set; }
+
+ ///
+ /// If the password is reset on save, this value will be populated
+ ///
+ [DataMember(Name = "resetPasswordValue")]
+ [ReadOnly(true)]
+ public string ResetPasswordValue { get; set; }
+
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/Models/ContentEditing/UserGroupBasic.cs b/src/Umbraco.Web/Models/ContentEditing/UserGroupBasic.cs
new file mode 100644
index 0000000000..b52a653497
--- /dev/null
+++ b/src/Umbraco.Web/Models/ContentEditing/UserGroupBasic.cs
@@ -0,0 +1,37 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.Serialization;
+
+namespace Umbraco.Web.Models.ContentEditing
+{
+ [DataContract(Name = "userGroup", Namespace = "")]
+ public class UserGroupBasic : EntityBasic, INotificationModel
+ {
+ public UserGroupBasic()
+ {
+ Notifications = new List();
+ Sections = Enumerable.Empty();
+ }
+
+ ///
+ /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes.
+ ///
+ [DataMember(Name = "notifications")]
+ public List Notifications { get; private set; }
+
+ [DataMember(Name = "sections")]
+ public IEnumerable