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