diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 477b5b55ea..ace98f2139 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -11,5 +11,5 @@ using System.Resources; [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyFileVersion("7.8.0")] -[assembly: AssemblyInformationalVersion("7.8.0-beta009")] \ No newline at end of file +[assembly: AssemblyFileVersion("7.9.0")] +[assembly: AssemblyInformationalVersion("7.9.0")] \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Consent.cs b/src/Umbraco.Core/Models/Consent.cs new file mode 100644 index 0000000000..bd0fb9b632 --- /dev/null +++ b/src/Umbraco.Core/Models/Consent.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.Serialization; +using Umbraco.Core.Models.EntityBase; + +namespace Umbraco.Core.Models +{ + /// + /// Represents a consent. + /// + [Serializable] + [DataContract(IsReference = true)] + internal class Consent : Entity, IConsent + { + private static PropertySelectors _selector; + + private bool _current; + private string _source; + private string _context; + private string _action; + private ConsentState _state; + private string _comment; + + // ReSharper disable once ClassNeverInstantiated.Local + private class PropertySelectors + { + public readonly PropertyInfo Current = ExpressionHelper.GetPropertyInfo(x => x.Current); + public readonly PropertyInfo Source = ExpressionHelper.GetPropertyInfo(x => x.Source); + public readonly PropertyInfo Context = ExpressionHelper.GetPropertyInfo(x => x.Context); + public readonly PropertyInfo Action = ExpressionHelper.GetPropertyInfo(x => x.Action); + public readonly PropertyInfo State = ExpressionHelper.GetPropertyInfo(x => x.State); + public readonly PropertyInfo Comment = ExpressionHelper.GetPropertyInfo(x => x.Comment); + } + + private static PropertySelectors Selectors => _selector ?? (_selector = new PropertySelectors()); + + /// + public bool Current + { + get => _current; + set => SetPropertyValueAndDetectChanges(value, ref _current, Selectors.Current); + } + + /// + public string Source + { + get => _source; + set + { + if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException(nameof(value)); + SetPropertyValueAndDetectChanges(value, ref _source, Selectors.Source); + } + } + + /// + public string Context + { + get => _context; + set + { + if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException(nameof(value)); + SetPropertyValueAndDetectChanges(value, ref _context, Selectors.Context); + } + } + + /// + public string Action + { + get => _action; + set + { + if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException(nameof(value)); + SetPropertyValueAndDetectChanges(value, ref _action, Selectors.Action); + } + } + + /// + public ConsentState State + { + get => _state; + // note: we probably should validate the state here, but since the + // enum is [Flags] with many combinations, this could be expensive + set => SetPropertyValueAndDetectChanges(value, ref _state, Selectors.State); + } + + /// + public string Comment + { + get => _comment; + set => SetPropertyValueAndDetectChanges(value, ref _comment, Selectors.Comment); + } + + /// + public IEnumerable History => HistoryInternal; + + /// + /// Gets the previous states of this consent. + /// + public List HistoryInternal { get; set; } + } +} diff --git a/src/Umbraco.Core/Models/ConsentExtensions.cs b/src/Umbraco.Core/Models/ConsentExtensions.cs new file mode 100644 index 0000000000..fabeaf5809 --- /dev/null +++ b/src/Umbraco.Core/Models/ConsentExtensions.cs @@ -0,0 +1,18 @@ +namespace Umbraco.Core.Models +{ + /// + /// Provides extension methods for the interface. + /// + public static class ConsentExtensions + { + /// + /// Determines whether the consent is granted. + /// + public static bool IsGranted(this IConsent consent) => (consent.State & ConsentState.Granted) > 0; + + /// + /// Determines whether the consent is revoked. + /// + public static bool IsRevoked(this IConsent consent) => (consent.State & ConsentState.Revoked) > 0; + } +} diff --git a/src/Umbraco.Core/Models/ConsentState.cs b/src/Umbraco.Core/Models/ConsentState.cs new file mode 100644 index 0000000000..ed370823f3 --- /dev/null +++ b/src/Umbraco.Core/Models/ConsentState.cs @@ -0,0 +1,38 @@ +using System; + +namespace Umbraco.Core.Models +{ + /// + /// Represents the state of a consent. + /// + [Flags] + public enum ConsentState // : int + { + // note - this is a [Flags] enumeration + // on can create detailed flags such as: + //GrantedOptIn = Granted | 0x0001 + //GrandedByForce = Granted | 0x0002 + // + // 16 situations for each Pending/Granted/Revoked should be ok + + /// + /// There is no consent. + /// + None = 0, + + /// + /// Consent is pending and has not been granted yet. + /// + Pending = 0x10000, + + /// + /// Consent has been granted. + /// + Granted = 0x20000, + + /// + /// Consent has been revoked. + /// + Revoked = 0x40000 + } +} diff --git a/src/Umbraco.Core/Models/IConsent.cs b/src/Umbraco.Core/Models/IConsent.cs new file mode 100644 index 0000000000..7c06420206 --- /dev/null +++ b/src/Umbraco.Core/Models/IConsent.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using Umbraco.Core.Models.EntityBase; + +namespace Umbraco.Core.Models +{ + /// + /// Represents a consent state. + /// + /// + /// A consent is fully identified by a source (whoever is consenting), a context (for + /// example, an application), and an action (whatever is consented). + /// A consent state registers the state of the consent (granted, revoked...). + /// + public interface IConsent : IAggregateRoot, IRememberBeingDirty + { + /// + /// Determines whether the consent entity represents the current state. + /// + bool Current { get; } + + /// + /// Gets the unique identifier of whoever is consenting. + /// + string Source { get; } + + /// + /// Gets the unique identifier of the context of the consent. + /// + /// + /// Represents the domain, application, scope... of the action. + /// When the action is a Udi, this should be the Udi type. + /// + string Context { get; } + + /// + /// Gets the unique identifier of the consented action. + /// + string Action { get; } + + /// + /// Gets the state of the consent. + /// + ConsentState State { get; } + + /// + /// Gets some additional free text. + /// + string Comment { get; } + + /// + /// Gets the previous states of this consent. + /// + IEnumerable History { get; } + } +} diff --git a/src/Umbraco.Core/Models/Rdbms/ConsentDto.cs b/src/Umbraco.Core/Models/Rdbms/ConsentDto.cs new file mode 100644 index 0000000000..0b6481070a --- /dev/null +++ b/src/Umbraco.Core/Models/Rdbms/ConsentDto.cs @@ -0,0 +1,44 @@ +using System; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.DatabaseAnnotations; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; + +namespace Umbraco.Core.Models.Rdbms +{ + [TableName(TableName)] + [PrimaryKey("id")] + [ExplicitColumns] + public class ConsentDto + { + internal const string TableName = "umbracoConsent"; + + [Column("id")] + [PrimaryKeyColumn] + public int Id { get; set; } + + [Column("current")] + public bool Current { get; set; } + + [Column("source")] + [Length(512)] + public string Source { get; set; } + + [Column("context")] + [Length(128)] + public string Context { get; set; } + + [Column("action")] + [Length(512)] + public string Action { get; set; } + + [Column("createDate")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime CreateDate { get; set; } + + [Column("state")] + public int State { get; set; } + + [Column("comment")] + public string Comment { get; set; } + } +} diff --git a/src/Umbraco.Core/Persistence/Factories/ConsentFactory.cs b/src/Umbraco.Core/Persistence/Factories/ConsentFactory.cs new file mode 100644 index 0000000000..bf42193853 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Factories/ConsentFactory.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Rdbms; + +namespace Umbraco.Core.Persistence.Factories +{ + internal static class ConsentFactory + { + public static IEnumerable BuildEntities(IEnumerable dtos) + { + var ix = new Dictionary(); + var output = new List(); + + foreach (var dto in dtos) + { + var k = dto.Source + "::" + dto.Context + "::" + dto.Action; + + var consent = new Consent + { + Id = dto.Id, + Current = dto.Current, + CreateDate = dto.CreateDate, + Source = dto.Source, + Context = dto.Context, + Action = dto.Action, + State = (ConsentState) dto.State, // assume value is valid + Comment = dto.Comment + }; + + //on initial construction we don't want to have dirty properties tracked + // http://issues.umbraco.org/issue/U4-1946 + consent.ResetDirtyProperties(false); + + if (ix.TryGetValue(k, out var current)) + { + if (current.HistoryInternal == null) + current.HistoryInternal = new List(); + current.HistoryInternal.Add(consent); + } + else + { + ix[k] = consent; + output.Add(consent); + } + } + + return output; + } + + public static ConsentDto BuildDto(IConsent entity) + { + return new ConsentDto + { + Id = entity.Id, + Current = entity.Current, + CreateDate = entity.CreateDate, + Source = entity.Source, + Context = entity.Context, + Action = entity.Action, + State = (int) entity.State, + Comment = entity.Comment + }; + } + } +} diff --git a/src/Umbraco.Core/Persistence/Mappers/ConsentMapper.cs b/src/Umbraco.Core/Persistence/Mappers/ConsentMapper.cs new file mode 100644 index 0000000000..b608339cc9 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Mappers/ConsentMapper.cs @@ -0,0 +1,40 @@ +using System.Collections.Concurrent; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Rdbms; + +namespace Umbraco.Core.Persistence.Mappers +{ + /// + /// Represents a mapper for consent entities. + /// + [MapperFor(typeof(IConsent))] + [MapperFor(typeof(Consent))] + public sealed class ConsentMapper : BaseMapper + { + private static readonly ConcurrentDictionary PropertyInfoCacheInstance + = new ConcurrentDictionary(); + + /// + /// Initializes a new instance of the class. + /// + public ConsentMapper() + { + // note: why the base ctor does not invoke BuildMap is a mystery to me + BuildMap(); + } + + internal override ConcurrentDictionary PropertyInfoCache => PropertyInfoCacheInstance; + + internal override void BuildMap() + { + CacheMap(entity => entity.Id, dto => dto.Id); + CacheMap(entity => entity.Current, dto => dto.Current); + CacheMap(entity => entity.CreateDate, dto => dto.CreateDate); + CacheMap(entity => entity.Source, dto => dto.Source); + CacheMap(entity => entity.Context, dto => dto.Context); + CacheMap(entity => entity.Action, dto => dto.Action); + CacheMap(entity => entity.State, dto => dto.State); + CacheMap(entity => entity.Comment, dto => dto.Comment); + } + } +} diff --git a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs index d1741b4ee0..2c6c54be75 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs @@ -92,7 +92,8 @@ namespace Umbraco.Core.Persistence.Migrations.Initial {52, typeof (UserGroup2NodePermissionDto) }, {53, typeof (UserGroup2AppDto) }, {54, typeof (UserStartNodeDto) }, - {55, typeof (UserLoginDto)} + {55, typeof (UserLoginDto)}, + {56, typeof (ConsentDto)} }; #endregion @@ -378,4 +379,4 @@ namespace Umbraco.Core.Persistence.Migrations.Initial #endregion } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenNineZero/AddUmbracoConsentTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenNineZero/AddUmbracoConsentTable.cs new file mode 100644 index 0000000000..bffa88eebd --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenNineZero/AddUmbracoConsentTable.cs @@ -0,0 +1,29 @@ +using System.Linq; +using Umbraco.Core.Logging; +using Umbraco.Core.Models.Rdbms; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenNineZero +{ + [Migration("7.9.0", 1, Constants.System.UmbracoMigrationName)] + public class AddUmbracoConsentTable : MigrationBase + { + public AddUmbracoConsentTable(ISqlSyntaxProvider sqlSyntax, ILogger logger) : base(sqlSyntax, logger) + { + } + + public override void Up() + { + var tables = SqlSyntax.GetTablesInSchema(Context.Database).ToArray(); + + if (tables.InvariantContains(ConsentDto.TableName)) + return; + + Create.Table(); + } + + public override void Down() + { + } + } +} diff --git a/src/Umbraco.Core/Persistence/Repositories/ConsentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ConsentRepository.cs new file mode 100644 index 0000000000..0485c76d62 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/ConsentRepository.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Models.EntityBase; +using Umbraco.Core.Models.Rdbms; +using Umbraco.Core.Persistence.Factories; +using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.Persistence.SqlSyntax; +using Umbraco.Core.Persistence.UnitOfWork; + +namespace Umbraco.Core.Persistence.Repositories +{ + /// + /// Represents the PetaPoco implementation of . + /// + internal class ConsentRepository : PetaPocoRepositoryBase, IConsentRepository + { + /// + /// Initializes a new instance of the class. + /// + public ConsentRepository(IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) + : base(work, cache, logger, sqlSyntax) + { } + + /// + protected override Guid NodeObjectTypeId => throw new NotSupportedException(); + + /// + public void ClearCurrent(string source, string context, string action) + { + var sql = new Sql($@"UPDATE {SqlSyntax.GetQuotedTableName(ConsentDto.TableName)} +SET {SqlSyntax.GetQuotedColumnName("current")} = @0 +WHERE {SqlSyntax.GetQuotedColumnName("source")} = @1 AND {SqlSyntax.GetQuotedColumnName("context")} = @2 AND {SqlSyntax.GetQuotedColumnName("action")} = @3 +AND {SqlSyntax.GetQuotedColumnName("current")} <> @0 ", false, source, context, action); + + Database.Execute(sql); + } + + /// + protected override IConsent PerformGet(int id) + { + throw new NotImplementedException(); + } + + /// + protected override IEnumerable PerformGetAll(params int[] ids) + { + throw new NotImplementedException(); + } + + /// + protected override IEnumerable PerformGetByQuery(IQuery query) + { + var sqlClause = new Sql().Select("*").From(SqlSyntax); + var translator = new SqlTranslator(sqlClause, query); + var sql = translator.Translate().OrderByDescending(x => x.CreateDate, SqlSyntax); + return ConsentFactory.BuildEntities(Database.Fetch(sql)); + } + + /// + protected override Sql GetBaseQuery(bool isCount) + { + throw new NotImplementedException(); + } + + /// + protected override string GetBaseWhereClause() + { + throw new NotImplementedException(); + } + + /// + protected override IEnumerable GetDeleteClauses() + { + throw new NotImplementedException(); + } + + /// + protected override void PersistNewItem(IConsent entity) + { + ((Entity) entity).AddingEntity(); + + var dto = ConsentFactory.BuildDto(entity); + Database.Insert(dto); + entity.Id = dto.Id; + entity.ResetDirtyProperties(); + } + + /// + protected override void PersistUpdatedItem(IConsent entity) + { + ((Entity) entity).UpdatingEntity(); + + var dto = ConsentFactory.BuildDto(entity); + Database.Update(dto); + entity.ResetDirtyProperties(); + + IsolatedCache.ClearCacheItem(GetCacheIdKey(entity.Id)); + } + } +} diff --git a/src/Umbraco.Core/Persistence/Repositories/IConsentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IConsentRepository.cs new file mode 100644 index 0000000000..9681bc575b --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/IConsentRepository.cs @@ -0,0 +1,15 @@ +using Umbraco.Core.Models; + +namespace Umbraco.Core.Persistence.Repositories +{ + /// + /// Represents a repository for entities. + /// + public interface IConsentRepository : IRepositoryQueryable + { + /// + /// Clears the current flag. + /// + void ClearCurrent(string source, string context, string action); + } +} diff --git a/src/Umbraco.Core/Persistence/RepositoryFactory.cs b/src/Umbraco.Core/Persistence/RepositoryFactory.cs index 34a4b1a612..af64de9d45 100644 --- a/src/Umbraco.Core/Persistence/RepositoryFactory.cs +++ b/src/Umbraco.Core/Persistence/RepositoryFactory.cs @@ -397,5 +397,10 @@ namespace Umbraco.Core.Persistence _logger, _sqlSyntax); } + + public IConsentRepository CreateConsentRepository(IScopeUnitOfWork uow) + { + return new ConsentRepository(uow, _cacheHelper, _logger, _sqlSyntax); + } } } diff --git a/src/Umbraco.Core/Services/ConsentService.cs b/src/Umbraco.Core/Services/ConsentService.cs new file mode 100644 index 0000000000..e36fda06c6 --- /dev/null +++ b/src/Umbraco.Core/Services/ConsentService.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using Umbraco.Core.Events; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.Persistence.UnitOfWork; + +namespace Umbraco.Core.Services +{ + /// + /// Implements . + /// + internal class ConsentService : ScopeRepositoryService, IConsentService + { + /// + /// Initializes a new instance of the class. + /// + public ConsentService(IScopeUnitOfWorkProvider provider, RepositoryFactory repositoryFactory, ILogger logger, IEventMessagesFactory eventMessagesFactory) + : base(provider, repositoryFactory, logger, eventMessagesFactory) + { } + + /// + public IConsent Register(string source, string context, string action, ConsentState state, string comment = null) + { + // prevent stupid states + var v = 0; + if ((state & ConsentState.Pending) > 0) v++; + if ((state & ConsentState.Granted) > 0) v++; + if ((state & ConsentState.Revoked) > 0) v++; + if (v != 1) + throw new ArgumentException("Invalid state.", nameof(state)); + + var consent = new Consent + { + Current = true, + Source = source, + Context = context, + Action = action, + CreateDate = DateTime.Now, + State = state, + Comment = comment + }; + + using (var uow = UowProvider.GetUnitOfWork()) + { + var repository = RepositoryFactory.CreateConsentRepository(uow); + repository.ClearCurrent(source, context, action); + repository.AddOrUpdate(consent); + uow.Commit(); + } + + return consent; + } + + /// + public IEnumerable Get(string source = null, string context = null, string action = null, + bool sourceStartsWith = false, bool contextStartsWith = false, bool actionStartsWith = false, + bool includeHistory = false) + { + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateConsentRepository(uow); + + IQuery query = new Query(); + + if (string.IsNullOrWhiteSpace(source) == false) + query = sourceStartsWith ? query.Where(x => x.Source.StartsWith(source)) : query.Where(x => x.Source == source); + if (string.IsNullOrWhiteSpace(context) == false) + query = contextStartsWith ? query.Where(x => x.Context.StartsWith(context)) : query.Where(x => x.Context == context); + if (string.IsNullOrWhiteSpace(action) == false) + query = actionStartsWith ? query.Where(x => x.Action.StartsWith(action)) : query.Where(x => x.Action == action); + if (includeHistory == false) + query = query.Where(x => x.Current); + + return repository.GetByQuery(query); + } + } + } +} diff --git a/src/Umbraco.Core/Services/IConsentService.cs b/src/Umbraco.Core/Services/IConsentService.cs new file mode 100644 index 0000000000..f077e75821 --- /dev/null +++ b/src/Umbraco.Core/Services/IConsentService.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using Umbraco.Core.Models; + +namespace Umbraco.Core.Services +{ + /// + /// Represents a service for handling entities. + /// + /// + /// Consent can be given or revoked or changed via the method, which + /// creates a new entity to track the consent. Revoking a consent is performed by + /// registering a revoked consent. + /// A consent can be revoked, by registering a revoked consent, but cannot be deleted. + /// Getter methods return the current state of a consent, i.e. the latest + /// entity that was created. + /// + public interface IConsentService : IService + { + /// + /// Registers consent. + /// + /// The source, i.e. whoever is consenting. + /// + /// + /// The state of the consent. + /// Additional free text. + /// The corresponding consent entity. + IConsent Register(string source, string context, string action, ConsentState state, string comment = null); + + /// + /// Retrieves consents. + /// + /// The optional source. + /// The optional context. + /// The optional action. + /// Determines whether is a start pattern. + /// Determines whether is a start pattern. + /// Determines whether is a start pattern. + /// Determines whether to include the history of consents. + /// Consents matching the paramters. + IEnumerable Get(string source = null, string context = null, string action = null, + bool sourceStartsWith = false, bool contextStartsWith = false, bool actionStartsWith = false, + bool includeHistory = false); + } +} diff --git a/src/Umbraco.Core/Services/ServiceContext.cs b/src/Umbraco.Core/Services/ServiceContext.cs index 144d548b78..24596aefc3 100644 --- a/src/Umbraco.Core/Services/ServiceContext.cs +++ b/src/Umbraco.Core/Services/ServiceContext.cs @@ -62,38 +62,13 @@ namespace Umbraco.Core.Services private Lazy _notificationService; private Lazy _externalLoginService; private Lazy _redirectUrlService; + private Lazy _consentService; internal IdkMap IdkMap { get; private set; } /// /// public ctor - will generally just be used for unit testing all items are optional and if not specified, the defaults will be used /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// public ServiceContext( IContentService contentService = null, IMediaService mediaService = null, @@ -120,7 +95,8 @@ namespace Umbraco.Core.Services IPublicAccessService publicAccessService = null, IExternalLoginService externalLoginService = null, IMigrationEntryService migrationEntryService = null, - IRedirectUrlService redirectUrlService = null) + IRedirectUrlService redirectUrlService = null, + IConsentService consentService = null) { if (migrationEntryService != null) _migrationEntryService = new Lazy(() => migrationEntryService); if (externalLoginService != null) _externalLoginService = new Lazy(() => externalLoginService); @@ -148,6 +124,7 @@ namespace Umbraco.Core.Services if (macroService != null) _macroService = new Lazy(() => macroService); if (publicAccessService != null) _publicAccessService = new Lazy(() => publicAccessService); if (redirectUrlService != null) _redirectUrlService = new Lazy(() => redirectUrlService); + if (consentService != null) _consentService = new Lazy(() => consentService); } /// @@ -310,6 +287,9 @@ namespace Umbraco.Core.Services if (_redirectUrlService == null) _redirectUrlService = new Lazy(() => new RedirectUrlService(provider, repositoryFactory, logger, eventMessagesFactory)); + + if (_consentService == null) + _consentService = new Lazy(() => new ConsentService(provider, repositoryFactory, logger, eventMessagesFactory)); } internal IEventMessagesFactory EventMessagesFactory { get; private set; } @@ -526,5 +506,10 @@ namespace Umbraco.Core.Services { get { return _redirectUrlService.Value; } } + + /// + /// Gets the implementation. + /// + public IConsentService ConsentService => _consentService.Value; } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index b0cc303187..c12097a22e 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -25,6 +25,7 @@ prompt 4 false + latest pdbonly @@ -363,9 +364,13 @@ + + + + @@ -377,6 +382,7 @@ + @@ -541,6 +547,7 @@ + @@ -552,6 +559,7 @@ + @@ -626,10 +634,13 @@ + + + @@ -726,8 +737,10 @@ + + diff --git a/src/Umbraco.Core/Umbraco.Core.csproj.DotSettings b/src/Umbraco.Core/Umbraco.Core.csproj.DotSettings deleted file mode 100644 index 662f95686e..0000000000 --- a/src/Umbraco.Core/Umbraco.Core.csproj.DotSettings +++ /dev/null @@ -1,2 +0,0 @@ - - CSharp50 \ No newline at end of file diff --git a/src/Umbraco.Tests/Plugins/PluginManagerTests.cs b/src/Umbraco.Tests/Plugins/PluginManagerTests.cs index fa4e9d9d21..e45025f322 100644 --- a/src/Umbraco.Tests/Plugins/PluginManagerTests.cs +++ b/src/Umbraco.Tests/Plugins/PluginManagerTests.cs @@ -271,7 +271,7 @@ AnotherContentFinder public void Resolves_Assigned_Mappers() { var foundTypes1 = _manager.ResolveAssignedMapperTypes(); - Assert.AreEqual(29, foundTypes1.Count()); + Assert.AreEqual(30, foundTypes1.Count()); } [Test] @@ -390,4 +390,4 @@ AnotherContentFinder } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs b/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs index 81f7ce30bd..d05862f2ad 100644 --- a/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs +++ b/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs @@ -21,7 +21,7 @@ namespace Umbraco.Tests.PropertyEditors [TestFixture] public class ImageCropperTest { - private const string cropperJson1 = "{\"focalPoint\": {\"left\": 0.96,\"top\": 0.80827067669172936},\"src\": \"/media/1005/img_0671.jpg\",\"crops\": [{\"alias\":\"thumb\",\"width\": 100,\"height\": 100,\"coordinates\": {\"x1\": 0.58729977382575338,\"y1\": 0.055768992440203169,\"x2\": 0,\"y2\": 0.32457553600198386}}]}"; + private const string cropperJson1 = "{\"focalPoint\": {\"left\": 0.96,\"top\": 0.80827067669172936},\"src\": \"/media/1005/img_0671.jpg\",\"crops\": [{\"alias\":\"thumb\",\"width\": 100,\"height\": 100,\"coordinates\": {\"x1\": 0.58729977382575338,\"y1\": 0.055768992440203169,\"x2\": 0,\"y2\": 0.32457553600198386}}]}"; private const string cropperJson2 = "{\"focalPoint\": {\"left\": 0.98,\"top\": 0.80827067669172936},\"src\": \"/media/1005/img_0672.jpg\",\"crops\": [{\"alias\":\"thumb\",\"width\": 100,\"height\": 100,\"coordinates\": {\"x1\": 0.58729977382575338,\"y1\": 0.055768992440203169,\"x2\": 0,\"y2\": 0.32457553600198386}}]}"; private const string cropperJson3 = "{\"focalPoint\": {\"left\": 0.98,\"top\": 0.80827067669172936},\"src\": \"/media/1005/img_0672.jpg\",\"crops\": []}"; private const string mediaPath = "/media/1005/img_0671.jpg"; @@ -56,7 +56,7 @@ namespace Umbraco.Tests.PropertyEditors index++; } - Assert.AreEqual(index, 1); + Assert.AreEqual(index, 1); } [Test] @@ -136,7 +136,7 @@ namespace Umbraco.Tests.PropertyEditors } [TestCase(cropperJson1, cropperJson1, true)] - [TestCase(cropperJson1, cropperJson2, false)] + [TestCase(cropperJson1, cropperJson2, false)] public void CanConvertImageCropperPropertyEditor(string val1, string val2, bool expected) { try @@ -266,7 +266,7 @@ namespace Umbraco.Tests.PropertyEditors var urlStringPad = mediaPath.GetCropUrl(imageCropperValue: cropperJson1, width: 300, height: 150, imageCropMode: ImageCropMode.Pad); var urlStringMax = mediaPath.GetCropUrl(imageCropperValue: cropperJson1, width: 300, height: 150, imageCropMode: ImageCropMode.Max); var urlStringStretch = mediaPath.GetCropUrl(imageCropperValue: cropperJson1, width: 300, height: 150, imageCropMode: ImageCropMode.Stretch); - + Assert.AreEqual(mediaPath + "?mode=min&width=300&height=150", urlStringMin); Assert.AreEqual(mediaPath + "?mode=boxpad&width=300&height=150", urlStringBoxPad); Assert.AreEqual(mediaPath + "?mode=pad&width=300&height=150", urlStringPad); @@ -380,4 +380,4 @@ namespace Umbraco.Tests.PropertyEditors Assert.AreEqual(mediaPath + "?mode=pad&width=400&height=400&bgcolor=fff", urlString); } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Tests/Services/ConsentServiceTests.cs b/src/Umbraco.Tests/Services/ConsentServiceTests.cs new file mode 100644 index 0000000000..ac48cda415 --- /dev/null +++ b/src/Umbraco.Tests/Services/ConsentServiceTests.cs @@ -0,0 +1,125 @@ +using System; +using System.Linq; +using NUnit.Framework; +using Umbraco.Core.Models; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.Mappers; +using Umbraco.Tests.TestHelpers; + +namespace Umbraco.Tests.Services +{ + [TestFixture] + [DatabaseTestBehavior(DatabaseBehavior.NewDbFileAndSchemaPerFixture)] + public class ConsentServiceTests : BaseServiceTest + { + [SetUp] + public override void Initialize() + { + base.Initialize(); + } + + [TearDown] + public override void TearDown() + { + base.TearDown(); + } + + [Test] + public void CanCrudConsent() + { + // fixme - why isn't this set by the test base class? + Database.Mapper = new PetaPocoMapper(); + + var consentService = ServiceContext.ConsentService; + + // can register + + var consent = consentService.Register("user/1234", "app1", "do-something", ConsentState.Granted, "no comment"); + Assert.AreNotEqual(0, consent.Id); + + Assert.IsTrue(consent.Current); + Assert.AreEqual("user/1234", consent.Source); + Assert.AreEqual("app1", consent.Context); + Assert.AreEqual("do-something", consent.Action); + Assert.AreEqual(ConsentState.Granted, consent.State); + Assert.AreEqual("no comment", consent.Comment); + + Assert.IsTrue(consent.IsGranted()); + + // can register more + + consentService.Register("user/1234", "app1", "do-something-else", ConsentState.Granted, "no comment"); + consentService.Register("user/1236", "app1", "do-something", ConsentState.Granted, "no comment"); + consentService.Register("user/1237", "app2", "do-something", ConsentState.Granted, "no comment"); + + // can get by source + + var consents = consentService.Get(source: "user/1235").ToArray(); + Assert.IsEmpty(consents); + + consents = consentService.Get(source: "user/1234").ToArray(); + Assert.AreEqual(2, consents.Length); + Assert.IsTrue(consents.All(x => x.Source == "user/1234")); + Assert.IsTrue(consents.Any(x => x.Action == "do-something")); + Assert.IsTrue(consents.Any(x => x.Action == "do-something-else")); + + // can get by context + + consents = consentService.Get(context: "app3").ToArray(); + Assert.IsEmpty(consents); + + consents = consentService.Get(context: "app2").ToArray(); + Assert.AreEqual(1, consents.Length); + + consents = consentService.Get(context: "app1").ToArray(); + Assert.AreEqual(3, consents.Length); + Assert.IsTrue(consents.Any(x => x.Action == "do-something")); + Assert.IsTrue(consents.Any(x => x.Action == "do-something-else")); + + // can get by action + + consents = consentService.Get(action: "do-whatever").ToArray(); + Assert.IsEmpty(consents); + + consents = consentService.Get(context: "app1", action: "do-something").ToArray(); + Assert.AreEqual(2, consents.Length); + Assert.IsTrue(consents.All(x => x.Action == "do-something")); + Assert.IsTrue(consents.Any(x => x.Source == "user/1234")); + Assert.IsTrue(consents.Any(x => x.Source == "user/1236")); + + // can revoke + + consent = consentService.Register("user/1234", "app1", "do-something", ConsentState.Revoked, "no comment"); + + consents = consentService.Get(source: "user/1234", context: "app1", action: "do-something").ToArray(); + Assert.AreEqual(1, consents.Length); + Assert.IsTrue(consents[0].Current); + Assert.AreEqual(ConsentState.Revoked, consents[0].State); + + // can filter + + consents = consentService.Get(context: "app1", action: "do-", actionStartsWith: true).ToArray(); + Assert.AreEqual(3, consents.Length); + Assert.IsTrue(consents.All(x => x.Context == "app1")); + Assert.IsTrue(consents.All(x => x.Action.StartsWith("do-"))); + + // can get history + + consents = consentService.Get(source: "user/1234", context: "app1", action: "do-something", includeHistory: true).ToArray(); + Assert.AreEqual(1, consents.Length); + Assert.IsTrue(consents[0].Current); + Assert.AreEqual(ConsentState.Revoked, consents[0].State); + Assert.IsTrue(consents[0].IsRevoked()); + Assert.IsNotNull(consents[0].History); + var history = consents[0].History.ToArray(); + Assert.AreEqual(1, history.Length); + Assert.IsFalse(history[0].Current); + Assert.AreEqual(ConsentState.Granted, history[0].State); + + // cannot be stupid + + Assert.Throws(() => + consentService.Register("user/1234", "app1", "do-something", ConsentState.Granted | ConsentState.Revoked, "no comment")); + } + } +} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 82e4f8ee8f..52261b454f 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -190,6 +190,7 @@ + diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 8cbf553d12..d4cd37e938 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -1024,8 +1024,10 @@ xcopy "$(ProjectDir)"..\packages\SqlServerCE.4.0.0.1\x86\*.* "$(TargetDir)x86\" True True - 7800 + 7900 / + http://localhost:7900 + 7790 http://localhost:7800 7800 / diff --git a/src/umbraco.sln.DotSettings b/src/umbraco.sln.DotSettings index dd168f105b..0791fed591 100644 --- a/src/umbraco.sln.DotSettings +++ b/src/umbraco.sln.DotSettings @@ -16,5 +16,5 @@ Disposable construction HINT False - CSharp50 + CSharp72 \ No newline at end of file