2017-12-07 16:45:25 +01:00
using System ;
using System.Collections.Generic ;
using System.Linq ;
2020-09-17 09:42:55 +02:00
using Microsoft.Extensions.Logging ;
2017-12-07 16:45:25 +01:00
using NPoco ;
2021-02-09 10:22:42 +01:00
using Umbraco.Cms.Core ;
using Umbraco.Cms.Core.Cache ;
using Umbraco.Cms.Core.Models ;
using Umbraco.Cms.Core.Models.Entities ;
using Umbraco.Cms.Core.Persistence.Querying ;
using Umbraco.Cms.Core.Persistence.Repositories ;
using Umbraco.Cms.Core.PropertyEditors ;
using Umbraco.Cms.Core.Security ;
using Umbraco.Cms.Core.Serialization ;
using Umbraco.Cms.Core.Services ;
2017-12-07 16:45:25 +01:00
using Umbraco.Core.Cache ;
using Umbraco.Core.Models ;
2017-12-28 09:06:33 +01:00
using Umbraco.Core.Persistence.Dtos ;
2017-12-07 16:45:25 +01:00
using Umbraco.Core.Persistence.Factories ;
using Umbraco.Core.Persistence.Querying ;
2019-12-10 08:37:19 +01:00
using Umbraco.Core.PropertyEditors ;
2017-12-12 15:04:13 +01:00
using Umbraco.Core.Scoping ;
2019-12-17 16:30:26 +01:00
using Umbraco.Core.Security ;
2020-11-17 20:27:10 +01:00
using Umbraco.Core.Serialization ;
2018-09-18 11:53:33 +02:00
using Umbraco.Core.Services ;
2021-02-09 10:22:42 +01:00
using static Umbraco . Cms . Core . Persistence . SqlExtensionsStatics ;
2017-12-07 16:45:25 +01:00
namespace Umbraco.Core.Persistence.Repositories.Implement
{
/// <summary>
/// Represents a repository for doing CRUD operations for <see cref="IMember"/>
/// </summary>
2019-12-17 16:30:26 +01:00
public class MemberRepository : ContentRepositoryBase < int , IMember , MemberRepository > , IMemberRepository
2017-12-07 16:45:25 +01:00
{
private readonly IMemberTypeRepository _memberTypeRepository ;
private readonly ITagRepository _tagRepository ;
2019-12-17 16:30:26 +01:00
private readonly IPasswordHasher _passwordHasher ;
2020-11-17 20:27:10 +01:00
private readonly IJsonSerializer _serializer ;
2017-12-07 16:45:25 +01:00
private readonly IMemberGroupRepository _memberGroupRepository ;
2020-07-31 00:19:36 +10:00
private readonly IRepositoryCachePolicy < IMember , string > _memberByUsernameCachePolicy ;
2017-12-07 16:45:25 +01:00
2020-09-17 09:42:55 +02:00
public MemberRepository ( IScopeAccessor scopeAccessor , AppCaches cache , ILogger < MemberRepository > logger ,
2019-10-24 16:48:21 +11:00
IMemberTypeRepository memberTypeRepository , IMemberGroupRepository memberGroupRepository , ITagRepository tagRepository , ILanguageRepository languageRepository , IRelationRepository relationRepository , IRelationTypeRepository relationTypeRepository ,
2019-12-17 16:30:26 +01:00
IPasswordHasher passwordHasher ,
2019-12-11 08:13:51 +01:00
Lazy < PropertyEditorCollection > propertyEditors ,
DataValueReferenceFactoryCollection dataValueReferenceFactories ,
2020-11-17 20:27:10 +01:00
IDataTypeService dataTypeService ,
IJsonSerializer serializer )
2019-12-11 08:13:51 +01:00
: base ( scopeAccessor , cache , logger , languageRepository , relationRepository , relationTypeRepository , propertyEditors , dataValueReferenceFactories , dataTypeService )
2017-12-07 16:45:25 +01:00
{
_memberTypeRepository = memberTypeRepository ? ? throw new ArgumentNullException ( nameof ( memberTypeRepository ) ) ;
_tagRepository = tagRepository ? ? throw new ArgumentNullException ( nameof ( tagRepository ) ) ;
2019-12-17 16:30:26 +01:00
_passwordHasher = passwordHasher ;
2020-11-17 20:27:10 +01:00
_serializer = serializer ;
2017-12-07 16:45:25 +01:00
_memberGroupRepository = memberGroupRepository ;
2020-07-31 00:19:36 +10:00
_memberByUsernameCachePolicy = new DefaultRepositoryCachePolicy < IMember , string > ( GlobalIsolatedCache , ScopeAccessor , DefaultOptions ) ;
2017-12-07 16:45:25 +01:00
}
protected override MemberRepository This = > this ;
public override int RecycleBinId = > throw new NotSupportedException ( ) ;
#region Repository Base
2021-02-09 10:22:42 +01:00
protected override Guid NodeObjectTypeId = > Cms . Core . Constants . ObjectTypes . Member ;
2017-12-07 16:45:25 +01:00
protected override IMember PerformGet ( int id )
{
var sql = GetBaseQuery ( QueryType . Single )
. Where < NodeDto > ( x = > x . NodeId = = id )
. SelectTop ( 1 ) ;
var dto = Database . Fetch < MemberDto > ( sql ) . FirstOrDefault ( ) ;
return dto = = null
? null
: MapDtoToContent ( dto ) ;
}
protected override IEnumerable < IMember > PerformGetAll ( params int [ ] ids )
{
var sql = GetBaseQuery ( QueryType . Many ) ;
if ( ids . Any ( ) )
sql . WhereIn < NodeDto > ( x = > x . NodeId , ids ) ;
return MapDtosToContent ( Database . Fetch < MemberDto > ( sql ) ) ;
}
protected override IEnumerable < IMember > PerformGetByQuery ( IQuery < IMember > query )
{
var baseQuery = GetBaseQuery ( false ) ;
2019-01-26 09:42:14 -05:00
// TODO: why is this different from content/media?!
2017-12-07 16:45:25 +01:00
// check if the query is based on properties or not
var wheres = query . GetWhereClauses ( ) ;
2019-01-22 18:03:39 -05:00
//this is a pretty rudimentary check but will work, we just need to know if this query requires property
2017-12-07 16:45:25 +01:00
// level queries
if ( wheres . Any ( x = > x . Item1 . Contains ( "cmsPropertyType" ) ) )
{
var sqlWithProps = GetNodeIdQueryWithPropertyData ( ) ;
var translator = new SqlTranslator < IMember > ( sqlWithProps , query ) ;
var sql = translator . Translate ( ) ;
baseQuery . Append ( "WHERE umbracoNode.id IN (" + sql . SQL + ")" , sql . Arguments )
. OrderBy < NodeDto > ( x = > x . SortOrder ) ;
return MapDtosToContent ( Database . Fetch < MemberDto > ( baseQuery ) ) ;
}
else
{
var translator = new SqlTranslator < IMember > ( baseQuery , query ) ;
var sql = translator . Translate ( )
. OrderBy < NodeDto > ( x = > x . SortOrder ) ;
return MapDtosToContent ( Database . Fetch < MemberDto > ( sql ) ) ;
}
}
protected override Sql < ISqlContext > GetBaseQuery ( QueryType queryType )
{
return GetBaseQuery ( queryType , true ) ;
}
protected virtual Sql < ISqlContext > GetBaseQuery ( QueryType queryType , bool current )
{
var sql = SqlContext . Sql ( ) ;
2019-01-26 09:42:14 -05:00
switch ( queryType ) // TODO: pretend we still need these queries for now
2017-12-07 16:45:25 +01:00
{
case QueryType . Count :
sql = sql . SelectCount ( ) ;
break ;
case QueryType . Ids :
sql = sql . Select < MemberDto > ( x = > x . NodeId ) ;
break ;
case QueryType . Single :
case QueryType . Many :
sql = sql . Select < MemberDto > ( r = >
2018-10-18 14:16:54 +02:00
r . Select ( x = > x . ContentVersionDto )
. Select ( x = > x . ContentDto , r1 = >
r1 . Select ( x = > x . NodeDto ) ) )
// ContentRepositoryBase expects a variantName field to order by name
// so get it here, though for members it's just the plain node name
. AndSelect < NodeDto > ( x = > Alias ( x . Text , "variantName" ) ) ;
2017-12-07 16:45:25 +01:00
break ;
}
sql
. From < MemberDto > ( )
. InnerJoin < ContentDto > ( ) . On < MemberDto , ContentDto > ( left = > left . NodeId , right = > right . NodeId )
. InnerJoin < NodeDto > ( ) . On < ContentDto , NodeDto > ( left = > left . NodeId , right = > right . NodeId )
. InnerJoin < ContentVersionDto > ( ) . On < ContentDto , ContentVersionDto > ( left = > left . NodeId , right = > right . NodeId )
// joining the type so we can do a query against the member type - not sure if this adds much overhead or not?
// the execution plan says it doesn't so we'll go with that and in that case, it might be worth joining the content
2019-10-15 13:18:45 +02:00
// types by default on the document and media repos so we can query by content type there too.
2017-12-07 16:45:25 +01:00
. InnerJoin < ContentTypeDto > ( ) . On < ContentDto , ContentTypeDto > ( left = > left . ContentTypeId , right = > right . NodeId ) ;
sql . Where < NodeDto > ( x = > x . NodeObjectType = = NodeObjectTypeId ) ;
if ( current )
sql . Where < ContentVersionDto > ( x = > x . Current ) ; // always get the current version
return sql ;
}
2019-01-26 09:42:14 -05:00
// TODO: move that one up to Versionable! or better: kill it!
2017-12-07 16:45:25 +01:00
protected override Sql < ISqlContext > GetBaseQuery ( bool isCount )
{
return GetBaseQuery ( isCount ? QueryType . Count : QueryType . Single ) ;
}
2019-01-26 09:42:14 -05:00
protected override string GetBaseWhereClause ( ) // TODO: can we kill / refactor this?
2017-12-07 16:45:25 +01:00
{
return "umbracoNode.id = @id" ;
}
2019-01-26 09:42:14 -05:00
// TODO: document/understand that one
2017-12-07 16:45:25 +01:00
protected Sql < ISqlContext > GetNodeIdQueryWithPropertyData ( )
{
return Sql ( )
. Select ( "DISTINCT(umbracoNode.id)" )
. From < NodeDto > ( )
. InnerJoin < ContentDto > ( ) . On < NodeDto , ContentDto > ( ( left , right ) = > left . NodeId = = right . NodeId )
. InnerJoin < ContentTypeDto > ( ) . On < ContentDto , ContentTypeDto > ( ( left , right ) = > left . ContentTypeId = = right . NodeId )
. InnerJoin < ContentVersionDto > ( ) . On < NodeDto , ContentVersionDto > ( ( left , right ) = > left . NodeId = = right . NodeId )
. InnerJoin < MemberDto > ( ) . On < ContentDto , MemberDto > ( ( left , right ) = > left . NodeId = = right . NodeId )
. LeftJoin < PropertyTypeDto > ( ) . On < ContentDto , PropertyTypeDto > ( left = > left . ContentTypeId , right = > right . ContentTypeId )
2018-01-15 17:32:45 +01:00
. LeftJoin < DataTypeDto > ( ) . On < PropertyTypeDto , DataTypeDto > ( left = > left . DataTypeId , right = > right . NodeId )
2017-12-07 16:45:25 +01:00
. LeftJoin < PropertyDataDto > ( ) . On ( x = > x
. Where < PropertyDataDto , PropertyTypeDto > ( ( left , right ) = > left . PropertyTypeId = = right . Id )
. Where < PropertyDataDto , ContentVersionDto > ( ( left , right ) = > left . VersionId = = right . Id ) )
. Where < NodeDto > ( x = > x . NodeObjectType = = NodeObjectTypeId ) ;
}
protected override IEnumerable < string > GetDeleteClauses ( )
{
var list = new List < string >
{
"DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @id" ,
"DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @id" ,
"DELETE FROM umbracoRelation WHERE parentId = @id" ,
"DELETE FROM umbracoRelation WHERE childId = @id" ,
"DELETE FROM cmsTagRelationship WHERE nodeId = @id" ,
2021-02-09 10:22:42 +01:00
"DELETE FROM " + Cms . Core . Constants . DatabaseSchema . Tables . PropertyData + " WHERE versionId IN (SELECT id FROM " + Cms . Core . Constants . DatabaseSchema . Tables . ContentVersion + " WHERE nodeId = @id)" ,
2017-12-07 16:45:25 +01:00
"DELETE FROM cmsMember2MemberGroup WHERE Member = @id" ,
"DELETE FROM cmsMember WHERE nodeId = @id" ,
2021-02-09 10:22:42 +01:00
"DELETE FROM " + Cms . Core . Constants . DatabaseSchema . Tables . ContentVersion + " WHERE nodeId = @id" ,
"DELETE FROM " + Cms . Core . Constants . DatabaseSchema . Tables . Content + " WHERE nodeId = @id" ,
2017-12-07 16:45:25 +01:00
"DELETE FROM umbracoNode WHERE id = @id"
} ;
return list ;
}
#endregion
#region Versions
public override IEnumerable < IMember > GetAllVersions ( int nodeId )
{
var sql = GetBaseQuery ( QueryType . Many , false )
. Where < NodeDto > ( x = > x . NodeId = = nodeId )
. OrderByDescending < ContentVersionDto > ( x = > x . Current )
. AndByDescending < ContentVersionDto > ( x = > x . VersionDate ) ;
return MapDtosToContent ( Database . Fetch < MemberDto > ( sql ) , true ) ;
}
public override IMember GetVersion ( int versionId )
{
var sql = GetBaseQuery ( QueryType . Single )
. Where < ContentVersionDto > ( x = > x . Id = = versionId ) ;
var dto = Database . Fetch < MemberDto > ( sql ) . FirstOrDefault ( ) ;
return dto = = null ? null : MapDtoToContent ( dto ) ;
}
protected override void PerformDeleteVersion ( int id , int versionId )
{
// raise event first else potential FK issues
2017-12-12 15:04:13 +01:00
OnUowRemovingVersion ( new ScopedVersionEventArgs ( AmbientScope , id , versionId ) ) ;
2017-12-07 16:45:25 +01:00
Database . Delete < PropertyDataDto > ( "WHERE versionId = @VersionId" , new { versionId } ) ;
Database . Delete < ContentVersionDto > ( "WHERE versionId = @VersionId" , new { versionId } ) ;
}
#endregion
#region Persist
protected override void PersistNewItem ( IMember entity )
{
2019-06-28 09:19:11 +02:00
entity . AddingEntity ( ) ;
2017-12-07 16:45:25 +01:00
// ensure that strings don't contain characters that are invalid in xml
2019-01-26 09:42:14 -05:00
// TODO: do we really want to keep doing this here?
2017-12-07 16:45:25 +01:00
entity . SanitizeEntityPropertiesForXmlStorage ( ) ;
// create the dto
var dto = ContentBaseFactory . BuildDto ( entity ) ;
// derive path and level from parent
var parent = GetParentNodeDto ( entity . ParentId ) ;
var level = parent . Level + 1 ;
// get sort order
var sortOrder = GetNewChildSortOrder ( entity . ParentId , 0 ) ;
// persist the node dto
var nodeDto = dto . ContentDto . NodeDto ;
nodeDto . Path = parent . Path ;
nodeDto . Level = Convert . ToInt16 ( level ) ;
nodeDto . SortOrder = sortOrder ;
// see if there's a reserved identifier for this unique id
// and then either update or insert the node dto
var id = GetReservedId ( nodeDto . UniqueId ) ;
if ( id > 0 )
{
nodeDto . NodeId = id ;
nodeDto . Path = string . Concat ( parent . Path , "," , nodeDto . NodeId ) ;
nodeDto . ValidatePathWithException ( ) ;
Database . Update ( nodeDto ) ;
}
else
{
Database . Insert ( nodeDto ) ;
// update path, now that we have an id
nodeDto . Path = string . Concat ( parent . Path , "," , nodeDto . NodeId ) ;
nodeDto . ValidatePathWithException ( ) ;
Database . Update ( nodeDto ) ;
}
// update entity
entity . Id = nodeDto . NodeId ;
entity . Path = nodeDto . Path ;
entity . SortOrder = sortOrder ;
entity . Level = level ;
// persist the content dto
var contentDto = dto . ContentDto ;
contentDto . NodeId = nodeDto . NodeId ;
Database . Insert ( contentDto ) ;
// persist the content version dto
// assumes a new version id and version date (modified date) has been set
var contentVersionDto = dto . ContentVersionDto ;
contentVersionDto . NodeId = nodeDto . NodeId ;
contentVersionDto . Current = true ;
Database . Insert ( contentVersionDto ) ;
2020-10-22 05:30:35 +11:00
entity . VersionId = contentVersionDto . Id ;
2017-12-07 16:45:25 +01:00
// persist the member dto
dto . NodeId = nodeDto . NodeId ;
// if the password is empty, generate one with the special prefix
// this will hash the guid with a salt so should be nicely random
if ( entity . RawPasswordValue . IsNullOrWhiteSpace ( ) )
{
2019-12-17 16:30:26 +01:00
2021-02-09 10:22:42 +01:00
dto . Password = Cms . Core . Constants . Security . EmptyPasswordPrefix + _passwordHasher . HashPassword ( Guid . NewGuid ( ) . ToString ( "N" ) ) ;
2017-12-07 16:45:25 +01:00
entity . RawPasswordValue = dto . Password ;
}
Database . Insert ( dto ) ;
// persist the property data
2020-10-22 05:30:35 +11:00
InsertPropertyValues ( entity , 0 , out _ , out _ ) ;
2017-12-07 16:45:25 +01:00
2020-11-17 20:27:10 +01:00
SetEntityTags ( entity , _tagRepository , _serializer ) ;
2017-12-07 16:45:25 +01:00
2019-10-25 14:17:18 +11:00
PersistRelations ( entity ) ;
2017-12-12 15:04:13 +01:00
OnUowRefreshedEntity ( new ScopedEntityEventArgs ( AmbientScope , entity ) ) ;
2017-12-07 16:45:25 +01:00
entity . ResetDirtyProperties ( ) ;
}
protected override void PersistUpdatedItem ( IMember entity )
2021-02-09 10:22:42 +01:00
{
2017-12-07 16:45:25 +01:00
// update
2020-10-22 05:30:35 +11:00
entity . UpdatingEntity ( ) ;
2017-12-07 16:45:25 +01:00
// ensure that strings don't contain characters that are invalid in xml
2019-01-26 09:42:14 -05:00
// TODO: do we really want to keep doing this here?
2017-12-07 16:45:25 +01:00
entity . SanitizeEntityPropertiesForXmlStorage ( ) ;
// if parent has changed, get path, level and sort order
if ( entity . IsPropertyDirty ( "ParentId" ) )
{
var parent = GetParentNodeDto ( entity . ParentId ) ;
entity . Path = string . Concat ( parent . Path , "," , entity . Id ) ;
entity . Level = parent . Level + 1 ;
entity . SortOrder = GetNewChildSortOrder ( entity . ParentId , 0 ) ;
}
// create the dto
var dto = ContentBaseFactory . BuildDto ( entity ) ;
// update the node dto
var nodeDto = dto . ContentDto . NodeDto ;
Database . Update ( nodeDto ) ;
// update the content dto
Database . Update ( dto . ContentDto ) ;
// update the content version dto
Database . Update ( dto . ContentVersionDto ) ;
// update the member dto
// but only the changed columns, 'cos we cannot update password if empty
var changedCols = new List < string > ( ) ;
if ( entity . IsPropertyDirty ( "Email" ) )
changedCols . Add ( "Email" ) ;
if ( entity . IsPropertyDirty ( "Username" ) )
changedCols . Add ( "LoginName" ) ;
// do NOT update the password if it has not changed or if it is null or empty
if ( entity . IsPropertyDirty ( "RawPasswordValue" ) & & ! string . IsNullOrWhiteSpace ( entity . RawPasswordValue ) )
changedCols . Add ( "Password" ) ;
if ( changedCols . Count > 0 )
Database . Update ( dto , changedCols ) ;
2020-10-22 05:30:35 +11:00
ReplacePropertyValues ( entity , entity . VersionId , 0 , out _ , out _ ) ;
2017-12-07 16:45:25 +01:00
2020-11-17 20:27:10 +01:00
SetEntityTags ( entity , _tagRepository , _serializer ) ;
2017-12-07 16:45:25 +01:00
2019-10-25 14:17:18 +11:00
PersistRelations ( entity ) ;
2017-12-12 15:04:13 +01:00
OnUowRefreshedEntity ( new ScopedEntityEventArgs ( AmbientScope , entity ) ) ;
2017-12-07 16:45:25 +01:00
entity . ResetDirtyProperties ( ) ;
}
protected override void PersistDeletedItem ( IMember entity )
{
// raise event first else potential FK issues
2017-12-12 15:04:13 +01:00
OnUowRemovingEntity ( new ScopedEntityEventArgs ( AmbientScope , entity ) ) ;
2017-12-07 16:45:25 +01:00
base . PersistDeletedItem ( entity ) ;
}
#endregion
public IEnumerable < IMember > FindMembersInRole ( string roleName , string usernameToMatch , StringPropertyMatchType matchType = StringPropertyMatchType . StartsWith )
{
//get the group id
var grpQry = Query < IMemberGroup > ( ) . Where ( group = > group . Name . Equals ( roleName ) ) ;
var memberGroup = _memberGroupRepository . Get ( grpQry ) . FirstOrDefault ( ) ;
if ( memberGroup = = null ) return Enumerable . Empty < IMember > ( ) ;
// get the members by username
var query = Query < IMember > ( ) ;
switch ( matchType )
{
case StringPropertyMatchType . Exact :
query . Where ( member = > member . Username . Equals ( usernameToMatch ) ) ;
break ;
case StringPropertyMatchType . Contains :
query . Where ( member = > member . Username . Contains ( usernameToMatch ) ) ;
break ;
case StringPropertyMatchType . StartsWith :
query . Where ( member = > member . Username . StartsWith ( usernameToMatch ) ) ;
break ;
case StringPropertyMatchType . EndsWith :
query . Where ( member = > member . Username . EndsWith ( usernameToMatch ) ) ;
break ;
case StringPropertyMatchType . Wildcard :
query . Where ( member = > member . Username . SqlWildcard ( usernameToMatch , TextColumnType . NVarchar ) ) ;
break ;
default :
throw new ArgumentOutOfRangeException ( nameof ( matchType ) ) ;
}
var matchedMembers = Get ( query ) . ToArray ( ) ;
var membersInGroup = new List < IMember > ( ) ;
//then we need to filter the matched members that are in the role
//since the max sql params are 2100 on sql server, we'll reduce that to be safe for potentially other servers and run the queries in batches
var inGroups = matchedMembers . InGroupsOf ( 1000 ) ;
foreach ( var batch in inGroups )
{
var memberIdBatch = batch . Select ( x = > x . Id ) ;
var sql = Sql ( ) . SelectAll ( ) . From < Member2MemberGroupDto > ( )
. Where < Member2MemberGroupDto > ( dto = > dto . MemberGroup = = memberGroup . Id )
. Where ( "Member IN (@memberIds)" , new { memberIds = memberIdBatch } ) ;
var memberIdsInGroup = Database . Fetch < Member2MemberGroupDto > ( sql )
. Select ( x = > x . Member ) . ToArray ( ) ;
membersInGroup . AddRange ( matchedMembers . Where ( x = > memberIdsInGroup . Contains ( x . Id ) ) ) ;
}
return membersInGroup ;
}
/// <summary>
/// Get all members in a specific group
/// </summary>
/// <param name="groupName"></param>
/// <returns></returns>
public IEnumerable < IMember > GetByMemberGroup ( string groupName )
{
var grpQry = Query < IMemberGroup > ( ) . Where ( group = > group . Name . Equals ( groupName ) ) ;
var memberGroup = _memberGroupRepository . Get ( grpQry ) . FirstOrDefault ( ) ;
if ( memberGroup = = null ) return Enumerable . Empty < IMember > ( ) ;
var subQuery = Sql ( ) . Select ( "Member" ) . From < Member2MemberGroupDto > ( ) . Where < Member2MemberGroupDto > ( dto = > dto . MemberGroup = = memberGroup . Id ) ;
var sql = GetBaseQuery ( false )
2019-01-27 01:17:32 -05:00
// TODO: An inner join would be better, though I've read that the query optimizer will always turn a
2017-12-07 16:45:25 +01:00
// subquery with an IN clause into an inner join anyways.
. Append ( "WHERE umbracoNode.id IN (" + subQuery . SQL + ")" , subQuery . Arguments )
. OrderByDescending < ContentVersionDto > ( x = > x . VersionDate )
. OrderBy < NodeDto > ( x = > x . SortOrder ) ;
return MapDtosToContent ( Database . Fetch < MemberDto > ( sql ) ) ;
}
public bool Exists ( string username )
{
var sql = Sql ( )
. SelectCount ( )
. From < MemberDto > ( )
. Where < MemberDto > ( x = > x . LoginName = = username ) ;
return Database . ExecuteScalar < int > ( sql ) > 0 ;
}
public int GetCountByQuery ( IQuery < IMember > query )
{
var sqlWithProps = GetNodeIdQueryWithPropertyData ( ) ;
var translator = new SqlTranslator < IMember > ( sqlWithProps , query ) ;
var sql = translator . Translate ( ) ;
//get the COUNT base query
var fullSql = GetBaseQuery ( true )
. Append ( new Sql ( "WHERE umbracoNode.id IN (" + sql . SQL + ")" , sql . Arguments ) ) ;
return Database . ExecuteScalar < int > ( fullSql ) ;
}
2020-07-30 22:56:31 +10:00
/// <inheritdoc />
public void SetLastLogin ( string username , DateTime date )
{
2020-08-10 22:29:04 +10:00
// Important - these queries are designed to execute without an exclusive WriteLock taken in our distributed lock
// table. However due to the data that we are updating which relies on version data we cannot update this data
// without taking some locks, otherwise we'll end up with strange situations because when a member is updated, that operation
// deletes and re-inserts all property data. So if there are concurrent transactions, one deleting and re-inserting and another trying
// to update there can be problems. This is only an issue for cmsPropertyData, not umbracoContentVersion because that table just
// maintains a single row and it isn't deleted/re-inserted.
// So the important part here is the ForUpdate() call on the select to fetch the property data to update.
2020-07-30 22:56:31 +10:00
// Update the cms property value for the member
var sqlSelectTemplateProperty = SqlContext . Templates . Get ( "Umbraco.Core.MemberRepository.SetLastLogin1" , s = > s
. Select < PropertyDataDto > ( x = > x . Id )
. From < PropertyDataDto > ( )
. InnerJoin < PropertyTypeDto > ( ) . On < PropertyTypeDto , PropertyDataDto > ( ( l , r ) = > l . Id = = r . PropertyTypeId )
. InnerJoin < ContentVersionDto > ( ) . On < ContentVersionDto , PropertyDataDto > ( ( l , r ) = > l . Id = = r . VersionId )
. InnerJoin < NodeDto > ( ) . On < NodeDto , ContentVersionDto > ( ( l , r ) = > l . NodeId = = r . NodeId )
. InnerJoin < MemberDto > ( ) . On < MemberDto , NodeDto > ( ( l , r ) = > l . NodeId = = r . NodeId )
. Where < NodeDto > ( x = > x . NodeObjectType = = SqlTemplate . Arg < Guid > ( "nodeObjectType" ) )
. Where < PropertyTypeDto > ( x = > x . Alias = = SqlTemplate . Arg < string > ( "propertyTypeAlias" ) )
2020-08-10 22:29:04 +10:00
. Where < MemberDto > ( x = > x . LoginName = = SqlTemplate . Arg < string > ( "username" ) )
. ForUpdate ( ) ) ;
2021-02-09 10:22:42 +01:00
var sqlSelectProperty = sqlSelectTemplateProperty . Sql ( Cms . Core . Constants . ObjectTypes . Member , Cms . Core . Constants . Conventions . Member . LastLoginDate , username ) ;
2020-07-30 22:56:31 +10:00
var update = Sql ( )
. Update < PropertyDataDto > ( u = > u
. Set ( x = > x . DateValue , date ) )
. WhereIn < PropertyDataDto > ( x = > x . Id , sqlSelectProperty ) ;
Database . Execute ( update ) ;
// Update the umbracoContentVersion value for the member
var sqlSelectTemplateVersion = SqlContext . Templates . Get ( "Umbraco.Core.MemberRepository.SetLastLogin2" , s = > s
. Select < ContentVersionDto > ( x = > x . Id )
2021-02-09 10:22:42 +01:00
. From < ContentVersionDto > ( )
2020-07-30 22:56:31 +10:00
. InnerJoin < NodeDto > ( ) . On < NodeDto , ContentVersionDto > ( ( l , r ) = > l . NodeId = = r . NodeId )
. InnerJoin < MemberDto > ( ) . On < MemberDto , NodeDto > ( ( l , r ) = > l . NodeId = = r . NodeId )
. Where < NodeDto > ( x = > x . NodeObjectType = = SqlTemplate . Arg < Guid > ( "nodeObjectType" ) )
. Where < MemberDto > ( x = > x . LoginName = = SqlTemplate . Arg < string > ( "username" ) ) ) ;
2021-02-09 10:22:42 +01:00
var sqlSelectVersion = sqlSelectTemplateVersion . Sql ( Cms . Core . Constants . ObjectTypes . Member , username ) ;
2020-07-30 22:56:31 +10:00
Database . Execute ( Sql ( )
. Update < ContentVersionDto > ( u = > u
. Set ( x = > x . VersionDate , date ) )
. WhereIn < ContentVersionDto > ( x = > x . Id , sqlSelectVersion ) ) ;
}
2017-12-07 16:45:25 +01:00
/// <summary>
/// Gets paged member results.
/// </summary>
2018-09-18 11:53:33 +02:00
public override IEnumerable < IMember > GetPage ( IQuery < IMember > query ,
long pageIndex , int pageSize , out long totalRecords ,
IQuery < IMember > filter ,
Ordering ordering )
2017-12-07 16:45:25 +01:00
{
Sql < ISqlContext > filterSql = null ;
if ( filter ! = null )
{
filterSql = Sql ( ) ;
foreach ( var clause in filter . GetWhereClauses ( ) )
filterSql = filterSql . Append ( $"AND ({clause.Item1})" , clause . Item2 ) ;
}
2017-12-12 15:04:13 +01:00
return GetPage < MemberDto > ( query , pageIndex , pageSize , out totalRecords ,
2018-09-18 11:53:33 +02:00
x = > MapDtosToContent ( x ) ,
filterSql ,
ordering ) ;
2017-12-07 16:45:25 +01:00
}
2018-09-18 11:53:33 +02:00
protected override string ApplySystemOrdering ( ref Sql < ISqlContext > sql , Ordering ordering )
2017-12-07 16:45:25 +01:00
{
2018-09-18 11:53:33 +02:00
if ( ordering . OrderBy . InvariantEquals ( "email" ) )
return SqlSyntax . GetFieldName < MemberDto > ( x = > x . Email ) ;
if ( ordering . OrderBy . InvariantEquals ( "loginName" ) )
return SqlSyntax . GetFieldName < MemberDto > ( x = > x . LoginName ) ;
if ( ordering . OrderBy . InvariantEquals ( "userName" ) )
return SqlSyntax . GetFieldName < MemberDto > ( x = > x . LoginName ) ;
2017-12-07 16:45:25 +01:00
2019-10-15 13:18:45 +02:00
if ( ordering . OrderBy . InvariantEquals ( "updateDate" ) )
return SqlSyntax . GetFieldName < ContentVersionDto > ( x = > x . VersionDate ) ;
if ( ordering . OrderBy . InvariantEquals ( "createDate" ) )
return SqlSyntax . GetFieldName < NodeDto > ( x = > x . CreateDate ) ;
if ( ordering . OrderBy . InvariantEquals ( "contentTypeAlias" ) )
return SqlSyntax . GetFieldName < ContentTypeDto > ( x = > x . Alias ) ;
2018-09-18 11:53:33 +02:00
return base . ApplySystemOrdering ( ref sql , ordering ) ;
2017-12-07 16:45:25 +01:00
}
private IEnumerable < IMember > MapDtosToContent ( List < MemberDto > dtos , bool withCache = false )
{
var temps = new List < TempContent < Member > > ( ) ;
var contentTypes = new Dictionary < int , IMemberType > ( ) ;
var content = new Member [ dtos . Count ] ;
for ( var i = 0 ; i < dtos . Count ; i + + )
{
var dto = dtos [ i ] ;
if ( withCache )
{
// if the cache contains the (proper version of the) item, use it
var cached = IsolatedCache . GetCacheItem < IMember > ( RepositoryCacheKeys . GetKey < IMember > ( dto . NodeId ) ) ;
if ( cached ! = null & & cached . VersionId = = dto . ContentVersionDto . Id )
{
2019-01-21 15:39:19 +01:00
content [ i ] = ( Member ) cached ;
2017-12-07 16:45:25 +01:00
continue ;
}
}
// else, need to build it
// get the content type - the repository is full cache *but* still deep-clones
// whatever comes out of it, so use our own local index here to avoid this
var contentTypeId = dto . ContentDto . ContentTypeId ;
if ( contentTypes . TryGetValue ( contentTypeId , out var contentType ) = = false )
contentTypes [ contentTypeId ] = contentType = _memberTypeRepository . Get ( contentTypeId ) ;
var c = content [ i ] = ContentBaseFactory . BuildEntity ( dto , contentType ) ;
// need properties
var versionId = dto . ContentVersionDto . Id ;
temps . Add ( new TempContent < Member > ( dto . NodeId , versionId , 0 , contentType , c ) ) ;
}
// load all properties for all documents from database in 1 query - indexed by version id
var properties = GetPropertyCollections ( temps ) ;
2019-01-22 18:03:39 -05:00
// assign properties
2017-12-07 16:45:25 +01:00
foreach ( var temp in temps )
{
temp . Content . Properties = properties [ temp . VersionId ] ;
// reset dirty initial properties (U4-1946)
temp . Content . ResetDirtyProperties ( false ) ;
}
return content ;
}
private IMember MapDtoToContent ( MemberDto dto )
{
var memberType = _memberTypeRepository . Get ( dto . ContentDto . ContentTypeId ) ;
var member = ContentBaseFactory . BuildEntity ( dto , memberType ) ;
// get properties - indexed by version id
var versionId = dto . ContentVersionDto . Id ;
var temp = new TempContent < Member > ( dto . ContentDto . NodeId , versionId , 0 , memberType ) ;
var properties = GetPropertyCollections ( new List < TempContent < Member > > { temp } ) ;
member . Properties = properties [ versionId ] ;
// reset dirty initial properties (U4-1946)
member . ResetDirtyProperties ( false ) ;
return member ;
}
2020-07-31 00:19:36 +10:00
public IMember GetByUsername ( string username )
{
return _memberByUsernameCachePolicy . Get ( username , PerformGetByUsername , PerformGetAllByUsername ) ;
}
private IMember PerformGetByUsername ( string username )
{
var query = Query < IMember > ( ) . Where ( x = > x . Username . Equals ( username ) ) ;
return PerformGetByQuery ( query ) . FirstOrDefault ( ) ;
}
private IEnumerable < IMember > PerformGetAllByUsername ( params string [ ] usernames )
{
var query = Query < IMember > ( ) . WhereIn ( x = > x . Username , usernames ) ;
return PerformGetByQuery ( query ) ;
}
2017-12-07 16:45:25 +01:00
}
}