2018-06-29 19:52:40 +02:00
using System ;
using System.ComponentModel.DataAnnotations ;
using System.Linq ;
using System.Net ;
using System.Net.Http ;
using System.Net.Http.Formatting ;
using System.Net.Http.Headers ;
using System.Threading.Tasks ;
using System.Web ;
using System.Web.Http ;
using System.Web.Http.ModelBinding ;
using System.Web.Security ;
using AutoMapper ;
using Umbraco.Core ;
using Umbraco.Core.Logging ;
using Umbraco.Core.Models ;
using Umbraco.Core.Models.Membership ;
using Umbraco.Core.Persistence.DatabaseModelDefinitions ;
using Umbraco.Core.Security ;
using Umbraco.Core.Services ;
using Umbraco.Core.Services.Implement ;
using Umbraco.Web.Models.Mapping ;
using Umbraco.Web.WebApi ;
using Umbraco.Web.Models.ContentEditing ;
using Umbraco.Web.Mvc ;
using Umbraco.Web.WebApi.Filters ;
using Constants = Umbraco . Core . Constants ;
2018-07-17 14:23:07 +10:00
using System.Collections.Generic ;
2018-09-20 17:22:39 +02:00
using Umbraco.Core.Models.ContentEditing ;
2018-07-17 14:23:07 +10:00
using Umbraco.Core.PropertyEditors ;
2018-09-20 17:22:39 +02:00
using Umbraco.Web.ContentApps ;
2018-07-19 19:32:07 +10:00
using Umbraco.Web.Editors.Binders ;
using Umbraco.Web.Editors.Filters ;
2018-06-29 19:52:40 +02:00
namespace Umbraco.Web.Editors
{
/// <remarks>
/// This controller is decorated with the UmbracoApplicationAuthorizeAttribute which means that any user requesting
/// access to ALL of the methods on this controller will need access to the member application.
/// </remarks>
[PluginController("UmbracoApi")]
[UmbracoApplicationAuthorizeAttribute(Constants.Applications.Members)]
[OutgoingNoHyphenGuidFormat]
public class MemberController : ContentControllerBase
{
2018-07-17 14:23:07 +10:00
public MemberController ( PropertyEditorCollection propertyEditors )
{
_propertyEditors = propertyEditors ? ? throw new ArgumentNullException ( nameof ( propertyEditors ) ) ;
}
2018-06-29 19:52:40 +02:00
private readonly MembershipProvider _provider = Core . Security . MembershipProviderExtensions . GetMembersMembershipProvider ( ) ;
2018-07-17 14:23:07 +10:00
private readonly PropertyEditorCollection _propertyEditors ;
2018-06-29 19:52:40 +02:00
/// <summary>
/// Returns the currently configured membership scenario for members in umbraco
/// </summary>
/// <value></value>
protected MembershipScenario MembershipScenario
{
get { return Services . MemberService . GetMembershipScenario ( ) ; }
2018-04-20 00:59:23 +10:00
}
2018-06-29 19:52:40 +02:00
public PagedResult < MemberBasic > GetPagedResults (
int pageNumber = 1 ,
int pageSize = 100 ,
string orderBy = "username" ,
Direction orderDirection = Direction . Ascending ,
bool orderBySystemField = true ,
string filter = "" ,
string memberTypeAlias = null )
{
if ( pageNumber < = 0 | | pageSize < = 0 )
{
throw new NotSupportedException ( "Both pageNumber and pageSize must be greater than zero" ) ;
}
if ( MembershipScenario = = MembershipScenario . NativeUmbraco )
{
var members = Services . MemberService
. GetAll ( ( pageNumber - 1 ) , pageSize , out var totalRecords , orderBy , orderDirection , orderBySystemField , memberTypeAlias , filter ) . ToArray ( ) ;
if ( totalRecords = = 0 )
{
return new PagedResult < MemberBasic > ( 0 , 0 , 0 ) ;
}
var pagedResult = new PagedResult < MemberBasic > ( totalRecords , pageNumber , pageSize )
{
Items = members
2018-07-19 19:32:07 +10:00
. Select ( x = > Mapper . Map < MemberBasic > ( x ) )
2018-06-29 19:52:40 +02:00
} ;
return pagedResult ;
}
else
{
int totalRecords ;
MembershipUserCollection members ;
if ( filter . IsNullOrWhiteSpace ( ) )
{
members = _provider . GetAllUsers ( ( pageNumber - 1 ) , pageSize , out totalRecords ) ;
}
else
{
//we need to search!
//try by name first
members = _provider . FindUsersByName ( filter , ( pageNumber - 1 ) , pageSize , out totalRecords ) ;
if ( totalRecords = = 0 )
{
//try by email then
members = _provider . FindUsersByEmail ( filter , ( pageNumber - 1 ) , pageSize , out totalRecords ) ;
}
}
if ( totalRecords = = 0 )
{
return new PagedResult < MemberBasic > ( 0 , 0 , 0 ) ;
}
var pagedResult = new PagedResult < MemberBasic > ( totalRecords , pageNumber , pageSize )
{
Items = members
. Cast < MembershipUser > ( )
. Select ( Mapper . Map < MembershipUser , MemberBasic > )
} ;
return pagedResult ;
}
}
/// <summary>
/// Returns a display node with a list view to render members
/// </summary>
/// <param name="listName"></param>
/// <returns></returns>
public MemberListDisplay GetListNodeDisplay ( string listName )
{
2018-07-11 15:58:48 +10:00
var foundType = Services . MemberTypeService . Get ( listName ) ;
var name = foundType ! = null ? foundType . Name : listName ;
2018-07-17 14:23:07 +10:00
var apps = new List < ContentApp > ( ) ;
2018-09-20 17:22:39 +02:00
apps . Add ( ListViewContentAppDefinition . CreateContentApp ( Services . DataTypeService , _propertyEditors , listName , "member" ) ) ;
2018-07-17 14:23:07 +10:00
apps [ 0 ] . Active = true ;
2018-06-29 19:52:40 +02:00
var display = new MemberListDisplay
{
ContentTypeAlias = listName ,
2018-07-11 15:58:48 +10:00
ContentTypeName = name ,
2018-06-29 19:52:40 +02:00
Id = listName ,
IsContainer = true ,
2018-07-11 15:58:48 +10:00
Name = listName = = Constants . Conventions . MemberTypes . AllMembersListId ? "All Members" : name ,
2018-06-29 19:52:40 +02:00
Path = "-1," + listName ,
2018-07-17 14:23:07 +10:00
ParentId = - 1 ,
ContentApps = apps
2018-06-29 19:52:40 +02:00
} ;
return display ;
}
/// <summary>
/// Gets the content json for the member
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
[OutgoingEditorModelEvent]
public MemberDisplay GetByKey ( Guid key )
{
MembershipUser foundMembershipMember ;
MemberDisplay display ;
IMember foundMember ;
switch ( MembershipScenario )
{
case MembershipScenario . NativeUmbraco :
foundMember = Services . MemberService . GetByKey ( key ) ;
if ( foundMember = = null )
{
HandleContentNotFound ( key ) ;
}
2018-07-19 19:32:07 +10:00
return Mapper . Map < MemberDisplay > ( foundMember ) ;
2018-06-29 19:52:40 +02:00
case MembershipScenario . CustomProviderWithUmbracoLink :
//TODO: Support editing custom properties for members with a custom membership provider here.
//foundMember = Services.MemberService.GetByKey(key);
//if (foundMember == null)
//{
// HandleContentNotFound(key);
//}
//foundMembershipMember = Membership.GetUser(key, false);
//if (foundMembershipMember == null)
//{
// HandleContentNotFound(key);
//}
//display = Mapper.Map<MembershipUser, MemberDisplay>(foundMembershipMember);
////map the name over
//display.Name = foundMember.Name;
//return display;
case MembershipScenario . StandaloneCustomProvider :
default :
foundMembershipMember = _provider . GetUser ( key , false ) ;
if ( foundMembershipMember = = null )
{
HandleContentNotFound ( key ) ;
}
display = Mapper . Map < MembershipUser , MemberDisplay > ( foundMembershipMember ) ;
return display ;
}
}
/// <summary>
/// Gets an empty content item for the
/// </summary>
/// <param name="contentTypeAlias"></param>
/// <returns></returns>
[OutgoingEditorModelEvent]
public MemberDisplay GetEmpty ( string contentTypeAlias = null )
{
IMember emptyContent ;
switch ( MembershipScenario )
{
case MembershipScenario . NativeUmbraco :
if ( contentTypeAlias = = null )
{
throw new HttpResponseException ( HttpStatusCode . NotFound ) ;
}
var contentType = Services . MemberTypeService . Get ( contentTypeAlias ) ;
if ( contentType = = null )
{
throw new HttpResponseException ( HttpStatusCode . NotFound ) ;
}
var provider = Core . Security . MembershipProviderExtensions . GetMembersMembershipProvider ( ) ;
emptyContent = new Member ( contentType ) ;
emptyContent . AdditionalData [ "NewPassword" ] = Membership . GeneratePassword ( provider . MinRequiredPasswordLength , provider . MinRequiredNonAlphanumericCharacters ) ;
2018-07-19 19:32:07 +10:00
return Mapper . Map < MemberDisplay > ( emptyContent ) ;
2018-06-29 19:52:40 +02:00
case MembershipScenario . CustomProviderWithUmbracoLink :
//TODO: Support editing custom properties for members with a custom membership provider here.
case MembershipScenario . StandaloneCustomProvider :
default :
//we need to return a scaffold of a 'simple' member - basically just what a membership provider can edit
emptyContent = MemberService . CreateGenericMembershipProviderMember ( "" , "" , "" , "" ) ;
emptyContent . AdditionalData [ "NewPassword" ] = Membership . GeneratePassword ( Membership . MinRequiredPasswordLength , Membership . MinRequiredNonAlphanumericCharacters ) ;
2018-07-19 19:32:07 +10:00
return Mapper . Map < MemberDisplay > ( emptyContent ) ;
2018-06-29 19:52:40 +02:00
}
}
/// <summary>
/// Saves member
/// </summary>
/// <returns></returns>
[FileUploadCleanupFilter]
[OutgoingEditorModelEvent]
2018-07-19 19:32:07 +10:00
[MemberSaveValidation]
2018-06-29 19:52:40 +02:00
public MemberDisplay PostSave (
[ModelBinder(typeof(MemberBinder))]
MemberSave contentItem )
{
//If we've reached here it means:
// * Our model has been bound
// * and validated
// * 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
//This is a special case for when we're not using the umbraco membership provider - when this is the case
// we will not have a ContentTypeAlias set which means the model state will be invalid but we don't care about that
// so we'll remove that model state value
if ( MembershipScenario ! = MembershipScenario . NativeUmbraco )
{
ModelState . Remove ( "ContentTypeAlias" ) ;
//TODO: We're removing this because we are not displaying it but when we support the CustomProviderWithUmbracoLink scenario
// we will be able to have a real name associated so do not remove this state once that is implemented!
ModelState . Remove ( "Name" ) ;
}
//map the properties to the persisted entity
MapPropertyValues ( contentItem ) ;
//Unlike content/media - if there are errors for a member, we do NOT proceed to save them, we cannot so return the errors
if ( ModelState . IsValid = = false )
{
2018-07-19 19:32:07 +10:00
var forDisplay = Mapper . Map < MemberDisplay > ( contentItem . PersistedContent ) ;
2018-06-29 19:52:40 +02:00
forDisplay . Errors = ModelState . ToErrorDictionary ( ) ;
throw new HttpResponseException ( Request . CreateValidationErrorResponse ( forDisplay ) ) ;
}
//TODO: WE need to support this! - requires UI updates, etc...
if ( _provider . RequiresQuestionAndAnswer )
{
throw new NotSupportedException ( "Currently the member editor does not support providers that have RequiresQuestionAndAnswer specified" ) ;
}
//We're gonna look up the current roles now because the below code can cause
// events to be raised and developers could be manually adding roles to members in
// their handlers. If we don't look this up now there's a chance we'll just end up
// removing the roles they've assigned.
var currRoles = Roles . GetRolesForUser ( contentItem . PersistedContent . Username ) ;
//find the ones to remove and remove them
var rolesToRemove = currRoles . Except ( contentItem . Groups ) . ToArray ( ) ;
string generatedPassword = null ;
//Depending on the action we need to first do a create or update using the membership provider
// this ensures that passwords are formatted correclty and also performs the validation on the provider itself.
switch ( contentItem . Action )
{
case ContentSaveAction . Save :
generatedPassword = UpdateWithMembershipProvider ( contentItem ) ;
break ;
case ContentSaveAction . SaveNew :
MembershipCreateStatus status ;
CreateWithMembershipProvider ( contentItem , out status ) ;
// save the ID of the creator
contentItem . PersistedContent . CreatorId = Security . CurrentUser . Id ;
break ;
default :
//we don't support anything else for members
throw new HttpResponseException ( HttpStatusCode . NotFound ) ;
}
//If we've had problems creating/updating the user with the provider then return the error
if ( ModelState . IsValid = = false )
{
2018-07-19 19:32:07 +10:00
var forDisplay = Mapper . Map < MemberDisplay > ( contentItem . PersistedContent ) ;
2018-06-29 19:52:40 +02:00
forDisplay . Errors = ModelState . ToErrorDictionary ( ) ;
throw new HttpResponseException ( Request . CreateValidationErrorResponse ( forDisplay ) ) ;
}
//save the IMember -
//TODO: When we support the CustomProviderWithUmbracoLink scenario, we'll need to save the custom properties for that here too
if ( MembershipScenario = = MembershipScenario . NativeUmbraco )
{
//save the item
//NOTE: We are setting the password to NULL - this indicates to the system to not actually save the password
// so it will not get overwritten!
contentItem . PersistedContent . RawPasswordValue = null ;
//create/save the IMember
Services . MemberService . Save ( contentItem . PersistedContent ) ;
}
//Now let's do the role provider stuff - now that we've saved the content item (that is important since
// if we are changing the username, it must be persisted before looking up the member roles).
if ( rolesToRemove . Any ( ) )
{
Roles . RemoveUserFromRoles ( contentItem . PersistedContent . Username , rolesToRemove ) ;
}
//find the ones to add and add them
var toAdd = contentItem . Groups . Except ( currRoles ) . ToArray ( ) ;
if ( toAdd . Any ( ) )
{
//add the ones submitted
Roles . AddUserToRoles ( contentItem . PersistedContent . Username , toAdd ) ;
}
//set the generated password (if there was one) - in order to do this we'll chuck the gen'd password into the
// additional data of the IUmbracoEntity of the persisted item - then we can retrieve this in the model mapper and set
// the value to be given to the UI. Hooray for AdditionalData :)
contentItem . PersistedContent . AdditionalData [ "GeneratedPassword" ] = generatedPassword ;
//return the updated model
2018-07-19 19:32:07 +10:00
var display = Mapper . Map < MemberDisplay > ( contentItem . PersistedContent ) ;
2018-06-29 19:52:40 +02:00
//lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403
HandleInvalidModelState ( display ) ;
var localizedTextService = Services . TextService ;
//put the correct msgs in
switch ( contentItem . Action )
{
case ContentSaveAction . Save :
case ContentSaveAction . SaveNew :
display . AddSuccessNotification ( localizedTextService . Localize ( "speechBubbles/editMemberSaved" ) , localizedTextService . Localize ( "speechBubbles/editMemberSaved" ) ) ;
break ;
}
return display ;
}
/// <summary>
/// Maps the property values to the persisted entity
/// </summary>
/// <param name="contentItem"></param>
private void MapPropertyValues ( MemberSave contentItem )
{
UpdateName ( contentItem ) ;
//map the custom properties - this will already be set for new entities in our member binder
contentItem . PersistedContent . Email = contentItem . Email ;
contentItem . PersistedContent . Username = contentItem . Username ;
//use the base method to map the rest of the properties
2018-08-02 15:12:26 +10:00
base . MapPropertyValuesForPersistence < IMember , MemberSave > (
2018-06-29 19:52:40 +02:00
contentItem ,
2018-08-01 16:46:13 +10:00
contentItem . PropertyCollectionDto ,
2018-06-29 19:52:40 +02:00
( save , property ) = > property . GetValue ( ) , //get prop val
( save , property , v ) = > property . SetValue ( v ) ) ; //set prop val
}
/// <summary>
/// Update the membership user using the membership provider (for things like email, etc...)
/// If a password change is detected then we'll try that too.
/// </summary>
/// <param name="contentItem"></param>
/// <returns>
/// If the password has been reset then this method will return the reset/generated password, otherwise will return null.
/// </returns>
private string UpdateWithMembershipProvider ( MemberSave contentItem )
{
//Get the member from the provider
var membershipUser = _provider . GetUser ( contentItem . PersistedContent . Key , false ) ;
if ( membershipUser = = null )
{
//This should never happen! so we'll let it YSOD if it does.
throw new InvalidOperationException ( "Could not get member from membership provider " + _provider . Name + " with key " + contentItem . PersistedContent . Key ) ;
}
var shouldReFetchMember = false ;
var providedUserName = contentItem . PersistedContent . Username ;
//if the user doesn't have access to sensitive values, then we need to check if any of the built in member property types
//have been marked as sensitive. If that is the case we cannot change these persisted values no matter what value has been posted.
//There's only 3 special ones we need to deal with that are part of the MemberSave instance
if ( Security . CurrentUser . HasAccessToSensitiveData ( ) = = false )
{
var sensitiveProperties = contentItem . PersistedContent . ContentType
. PropertyTypes . Where ( x = > contentItem . PersistedContent . ContentType . IsSensitiveProperty ( x . Alias ) )
. ToList ( ) ;
foreach ( var sensitiveProperty in sensitiveProperties )
{
//if found, change the value of the contentItem model to the persisted value so it remains unchanged
switch ( sensitiveProperty . Alias )
{
case Constants . Conventions . Member . Comments :
contentItem . Comments = contentItem . PersistedContent . Comments ;
break ;
case Constants . Conventions . Member . IsApproved :
contentItem . IsApproved = contentItem . PersistedContent . IsApproved ;
break ;
case Constants . Conventions . Member . IsLockedOut :
contentItem . IsLockedOut = contentItem . PersistedContent . IsLockedOut ;
break ;
}
}
}
//Update the membership user if it has changed
try
{
var requiredUpdating = Members . UpdateMember ( membershipUser , _provider ,
contentItem . Email . Trim ( ) ,
contentItem . IsApproved ,
comment : contentItem . Comments ) ;
if ( requiredUpdating . Success )
{
//re-map these values
shouldReFetchMember = true ;
}
}
catch ( Exception ex )
{
Logger . Warn < MemberController > ( ex , "Could not update member, the provider returned an error" ) ;
ModelState . AddPropertyError (
//specify 'default' just so that it shows up as a notification - is not assigned to a property
new ValidationResult ( "Could not update member, the provider returned an error: " + ex . Message + " (see log for full details)" ) , "default" ) ;
}
//if they were locked but now they are trying to be unlocked
if ( membershipUser . IsLockedOut & & contentItem . IsLockedOut = = false )
{
try
{
var result = _provider . UnlockUser ( membershipUser . UserName ) ;
if ( result = = false )
{
//it wasn't successful - but it won't really tell us why.
ModelState . AddModelError ( "custom" , "Could not unlock the user" ) ;
}
else
{
shouldReFetchMember = true ;
}
}
catch ( Exception ex )
{
ModelState . AddModelError ( "custom" , ex ) ;
}
}
else if ( membershipUser . IsLockedOut = = false & & contentItem . IsLockedOut )
{
//NOTE: This should not ever happen unless someone is mucking around with the request data.
//An admin cannot simply lock a user, they get locked out by password attempts, but an admin can un-approve them
ModelState . AddModelError ( "custom" , "An admin cannot lock a user" ) ;
}
//password changes ?
if ( contentItem . Password = = null )
{
//If the provider has changed some values, these values need to be reflected in the member object
//that will get mapped to the display object
if ( shouldReFetchMember )
{
RefetchMemberData ( contentItem , LookupType . ByKey ) ;
RestoreProvidedUserName ( contentItem , providedUserName ) ;
}
return null ;
}
var passwordChangeResult = Members . ChangePassword ( membershipUser . UserName , contentItem . Password , _provider ) ;
if ( passwordChangeResult . Success )
{
//If the provider has changed some values, these values need to be reflected in the member object
//that will get mapped to the display object
if ( shouldReFetchMember )
{
RefetchMemberData ( contentItem , LookupType . ByKey ) ;
RestoreProvidedUserName ( contentItem , providedUserName ) ;
}
//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
return passwordChangeResult . Result . ResetPassword ;
}
//it wasn't successful, so add the change error to the model state
ModelState . AddPropertyError (
passwordChangeResult . Result . ChangeError ,
string . Format ( "{0}password" , Constants . PropertyEditors . InternalGenericPropertiesPrefix ) ) ;
return null ;
}
private enum LookupType
{
ByKey ,
ByUserName
}
/// <summary>
/// Re-fetches the database data to map to the PersistedContent object and re-assigns the already mapped the posted properties so that the display object is up-to-date
/// </summary>
/// <param name="contentItem"></param>
2018-07-19 19:32:07 +10:00
/// <param name="lookup"></param>
2018-06-29 19:52:40 +02:00
/// <remarks>
/// This is done during an update if the membership provider has changed some underlying data - we need to ensure that our model is consistent with that data
/// </remarks>
private void RefetchMemberData ( MemberSave contentItem , LookupType lookup )
{
var currProps = contentItem . PersistedContent . Properties . ToArray ( ) ;
switch ( MembershipScenario )
{
case MembershipScenario . NativeUmbraco :
switch ( lookup )
{
case LookupType . ByKey :
//Go and re-fetch the persisted item
contentItem . PersistedContent = Services . MemberService . GetByKey ( contentItem . Key ) ;
break ;
case LookupType . ByUserName :
contentItem . PersistedContent = Services . MemberService . GetByUsername ( contentItem . Username . Trim ( ) ) ;
break ;
}
break ;
case MembershipScenario . CustomProviderWithUmbracoLink :
case MembershipScenario . StandaloneCustomProvider :
default :
var membershipUser = _provider . GetUser ( contentItem . Key , false ) ;
//Go and re-fetch the persisted item
contentItem . PersistedContent = Mapper . Map < MembershipUser , IMember > ( membershipUser ) ;
break ;
}
UpdateName ( contentItem ) ;
//re-assign the mapped values that are not part of the membership provider properties.
var builtInAliases = Constants . Conventions . Member . GetStandardPropertyTypeStubs ( ) . Select ( x = > x . Key ) . ToArray ( ) ;
foreach ( var p in contentItem . PersistedContent . Properties )
{
var valueMapped = currProps . FirstOrDefault ( x = > x . Alias = = p . Alias ) ;
if ( builtInAliases . Contains ( p . Alias ) = = false & & valueMapped ! = null )
{
p . SetValue ( valueMapped . GetValue ( ) ) ;
// fixme - ok, I give up, at that point tags are dead here, until we figure it out
//p.TagChanges.Behavior = valueMapped.TagChanges.Behavior;
//p.TagChanges.Enable = valueMapped.TagChanges.Enable;
//p.TagChanges.Tags = valueMapped.TagChanges.Tags;
}
}
}
/// <summary>
/// Following a refresh of member data called during an update if the membership provider has changed some underlying data,
/// we don't want to lose the provided, and potentiallly changed, username
/// </summary>
/// <param name="contentItem"></param>
/// <param name="providedUserName"></param>
private static void RestoreProvidedUserName ( MemberSave contentItem , string providedUserName )
{
contentItem . PersistedContent . Username = providedUserName ;
}
private static void UpdateName ( MemberSave memberSave )
{
//Don't update the name if it is empty
if ( memberSave . Name . IsNullOrWhiteSpace ( ) = = false )
{
memberSave . PersistedContent . Name = memberSave . Name ;
}
}
/// <summary>
/// This is going to create the user with the membership provider and check for validation
/// </summary>
/// <param name="contentItem"></param>
/// <param name="status"></param>
/// <returns></returns>
/// <remarks>
/// Depending on if the Umbraco membership provider is active or not, the process differs slightly:
///
/// * If the umbraco membership provider is used - we create the membership user first with the membership provider, since
/// it's the umbraco membership provider, this writes to the umbraco tables. When that is complete we re-fetch the IMember
/// model data from the db. In this case we don't care what the provider user key is.
/// * If we're using a non-umbraco membership provider - we check if there is a 'Member' member type - if so
/// we create an empty IMember instance first (of type 'Member'), this gives us a unique ID (GUID)
/// that we then use to create the member in the custom membership provider. This acts as the link between Umbraco data and
/// the custom membership provider data. This gives us the ability to eventually have custom membership properties but still use
/// a custom memberhip provider. If there is no 'Member' member type, then we will simply just create the membership provider member
/// with no link to our data.
///
/// If this is successful, it will go and re-fetch the IMember from the db because it will now have an ID because the Umbraco provider
/// uses the umbraco data store - then of course we need to re-map it to the saved property values.
/// </remarks>
private MembershipUser CreateWithMembershipProvider ( MemberSave contentItem , out MembershipCreateStatus status )
{
MembershipUser membershipUser ;
switch ( MembershipScenario )
{
case MembershipScenario . NativeUmbraco :
//We are using the umbraco membership provider, create the member using the membership provider first.
var umbracoMembershipProvider = ( UmbracoMembershipProviderBase ) _provider ;
//TODO: We are not supporting q/a - passing in empty here
membershipUser = umbracoMembershipProvider . CreateUser (
contentItem . ContentTypeAlias , contentItem . Username ,
contentItem . Password . NewPassword ,
contentItem . Email , "" , "" ,
contentItem . IsApproved ,
Guid . NewGuid ( ) , //since it's the umbraco provider, the user key here doesn't make any difference
out status ) ;
break ;
case MembershipScenario . CustomProviderWithUmbracoLink :
//We are using a custom membership provider, we'll create an empty IMember first to get the unique id to use
// as the provider user key.
//create it - this persisted item has already been set in the MemberBinder based on the 'Member' member type:
Services . MemberService . Save ( contentItem . PersistedContent ) ;
//TODO: We are not supporting q/a - passing in empty here
membershipUser = _provider . CreateUser (
contentItem . Username ,
contentItem . Password . NewPassword ,
contentItem . Email ,
"TEMP" , //some membership provider's require something here even if q/a is disabled!
"TEMP" , //some membership provider's require something here even if q/a is disabled!
contentItem . IsApproved ,
contentItem . PersistedContent . Key , //custom membership provider, we'll link that based on the IMember unique id (GUID)
out status ) ;
break ;
case MembershipScenario . StandaloneCustomProvider :
// we don't have a member type to use so we will just create the basic membership user with the provider with no
// link back to the umbraco data
var newKey = Guid . NewGuid ( ) ;
//TODO: We are not supporting q/a - passing in empty here
membershipUser = _provider . CreateUser (
contentItem . Username ,
contentItem . Password . NewPassword ,
contentItem . Email ,
"TEMP" , //some membership provider's require something here even if q/a is disabled!
"TEMP" , //some membership provider's require something here even if q/a is disabled!
contentItem . IsApproved ,
newKey ,
out status ) ;
break ;
default :
throw new ArgumentOutOfRangeException ( ) ;
}
//TODO: Localize these!
switch ( status )
{
case MembershipCreateStatus . Success :
//map the key back
contentItem . Key = membershipUser . ProviderUserKey . TryConvertTo < Guid > ( ) . Result ;
contentItem . PersistedContent . Key = contentItem . Key ;
//if the comments are there then we need to save them
if ( contentItem . Comments . IsNullOrWhiteSpace ( ) = = false )
{
membershipUser . Comment = contentItem . Comments ;
_provider . UpdateUser ( membershipUser ) ;
}
RefetchMemberData ( contentItem , LookupType . ByUserName ) ;
break ;
case MembershipCreateStatus . InvalidUserName :
ModelState . AddPropertyError (
new ValidationResult ( "Invalid user name" , new [ ] { "value" } ) ,
string . Format ( "{0}login" , Constants . PropertyEditors . InternalGenericPropertiesPrefix ) ) ;
break ;
case MembershipCreateStatus . InvalidPassword :
ModelState . AddPropertyError (
new ValidationResult ( "Invalid password" , new [ ] { "value" } ) ,
string . Format ( "{0}password" , Constants . PropertyEditors . InternalGenericPropertiesPrefix ) ) ;
break ;
case MembershipCreateStatus . InvalidQuestion :
case MembershipCreateStatus . InvalidAnswer :
throw new NotSupportedException ( "Currently the member editor does not support providers that have RequiresQuestionAndAnswer specified" ) ;
case MembershipCreateStatus . InvalidEmail :
ModelState . AddPropertyError (
new ValidationResult ( "Invalid email" , new [ ] { "value" } ) ,
string . Format ( "{0}email" , Constants . PropertyEditors . InternalGenericPropertiesPrefix ) ) ;
break ;
case MembershipCreateStatus . DuplicateUserName :
ModelState . AddPropertyError (
new ValidationResult ( "Username is already in use" , new [ ] { "value" } ) ,
string . Format ( "{0}login" , Constants . PropertyEditors . InternalGenericPropertiesPrefix ) ) ;
break ;
case MembershipCreateStatus . DuplicateEmail :
ModelState . AddPropertyError (
new ValidationResult ( "Email address is already in use" , new [ ] { "value" } ) ,
string . Format ( "{0}email" , Constants . PropertyEditors . InternalGenericPropertiesPrefix ) ) ;
break ;
case MembershipCreateStatus . InvalidProviderUserKey :
ModelState . AddPropertyError (
//specify 'default' just so that it shows up as a notification - is not assigned to a property
new ValidationResult ( "Invalid provider user key" ) , "default" ) ;
break ;
case MembershipCreateStatus . DuplicateProviderUserKey :
ModelState . AddPropertyError (
//specify 'default' just so that it shows up as a notification - is not assigned to a property
new ValidationResult ( "Duplicate provider user key" ) , "default" ) ;
break ;
case MembershipCreateStatus . ProviderError :
case MembershipCreateStatus . UserRejected :
ModelState . AddPropertyError (
//specify 'default' just so that it shows up as a notification - is not assigned to a property
new ValidationResult ( "User could not be created (rejected by provider)" ) , "default" ) ;
break ;
default :
throw new ArgumentOutOfRangeException ( ) ;
}
return membershipUser ;
}
/// <summary>
/// Permanently deletes a member
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
///
[HttpPost]
public HttpResponseMessage DeleteByKey ( Guid key )
{
IMember foundMember ;
MembershipUser foundMembershipUser ;
switch ( MembershipScenario )
{
case MembershipScenario . NativeUmbraco :
foundMember = Services . MemberService . GetByKey ( key ) ;
if ( foundMember = = null )
{
return HandleContentNotFound ( key , false ) ;
}
Services . MemberService . Delete ( foundMember ) ;
break ;
case MembershipScenario . CustomProviderWithUmbracoLink :
foundMember = Services . MemberService . GetByKey ( key ) ;
if ( foundMember ! = null )
{
Services . MemberService . Delete ( foundMember ) ;
}
foundMembershipUser = _provider . GetUser ( key , false ) ;
if ( foundMembershipUser ! = null )
{
_provider . DeleteUser ( foundMembershipUser . UserName , true ) ;
}
break ;
case MembershipScenario . StandaloneCustomProvider :
foundMembershipUser = _provider . GetUser ( key , false ) ;
if ( foundMembershipUser ! = null )
{
_provider . DeleteUser ( foundMembershipUser . UserName , true ) ;
}
break ;
default :
throw new ArgumentOutOfRangeException ( ) ;
}
return Request . CreateResponse ( HttpStatusCode . OK ) ;
}
/// <summary>
/// Exports member data based on their unique Id
/// </summary>
/// <param name="key">The unique <see cref="Guid">member identifier</see></param>
/// <returns><see cref="HttpResponseMessage"/></returns>
[HttpGet]
public HttpResponseMessage ExportMemberData ( Guid key )
{
var currentUser = Security . CurrentUser ;
var httpResponseMessage = Request . CreateResponse ( ) ;
if ( currentUser . HasAccessToSensitiveData ( ) = = false )
{
httpResponseMessage . StatusCode = HttpStatusCode . Forbidden ;
return httpResponseMessage ;
}
var member = ( ( MemberService ) Services . MemberService ) . ExportMember ( key ) ;
var fileName = $"{member.Name}_{member.Email}.txt" ;
httpResponseMessage . Content = new ObjectContent < MemberExportModel > ( member , new JsonMediaTypeFormatter { Indent = true } ) ;
httpResponseMessage . Content . Headers . Add ( "x-filename" , fileName ) ;
httpResponseMessage . Content . Headers . ContentType = new MediaTypeHeaderValue ( "application/octet-stream" ) ;
httpResponseMessage . Content . Headers . ContentDisposition = new ContentDispositionHeaderValue ( "attachment" ) ;
httpResponseMessage . Content . Headers . ContentDisposition . FileName = fileName ;
httpResponseMessage . StatusCode = HttpStatusCode . OK ;
return httpResponseMessage ;
}
}
}