Data erasers - Clear contact information from the site

Some data protection laws require site owners to comply when visitors request that their personal data be removed from the application. Data erasers are used to delete or anonymize a visitor’s data. They are called from the Right to be forgotten tab in the Data protection app of the Xperience by Kentico administration application. This guide will show you how to write a data eraser.

Data protection series - prerequisites

This article is part of a series that showcases how you can implement data protection features inXperience by Kentico.

You can find the starting point in this repository if you wish to follow along. Instructions for setup are in the README file. In this case, we advise starting from the beginning of the Beginning of the Data protection series.

The example presented in this Data protection guide series represents a valid and complete implementation of data protection in Xperience by Kentico. You can simply copy-paste the code samples into your own solution.

However, if you choose to do so, make sure to consult your legal team to determine whether the implementation, texts, and consent levels meet the requirements of your region and market.

Add a data eraser

This guide’s data eraser is similar in some ways to the data collector from the previous guide, but it differs in a few key ways. Firstly, you don’t need to choose between different types of data writers in a data eraser because the data is being deleted instead of compiled for display. This means you don’t need to move the erasure logic into a separate file, as with the data collector.

This data eraser will follow the same principle as the documentation example, but it will include additional options for deleting form data and form submission activities.

Unlike the data collector, this class does not need collections of CollectedColumn objects. It will simply delete the objects rather than display specific fields.

Alternatively, you can make a data eraser that only anonymizes objects (overwrites columns with personal identifying information with anonymous values) instead of deleting them. If you decide to anonymize certain object types, you can take a similar approach and specify which columns you want to anonymize.

  1. Create a new Erasers folder in TrainingGuides.Web/Features/DataProtection and add a new file called ContactDataEraser.cs.

  2. Use the FormCollectionService from the previous guide to retrieve forms and form submissions.

  3. Include methods to delete form submission activities, all activities, contacts, and submitted form data.

    These methods correspond to the configuration options that users can choose on the Right to be forgotten tab of the Data protection application. You must check this configuration in each method to decide whether the object type should actually be deleted.

ContactDataEraser.cs


using CMS.Activities;
using CMS.Base;
using CMS.ContactManagement;
using CMS.DataEngine;
using CMS.DataProtection;
using CMS.Helpers;
using CMS.OnlineForms;
using TrainingGuides.Web.Features.DataProtection.Collectors;
using TrainingGuides.Web.Features.DataProtection.Services;

namespace TrainingGuides.Web.Features.DataProtection.Erasers;

public class ContactDataEraser : IPersonalDataEraser
{
    private readonly IFormCollectionService formCollectionService;
    private readonly Dictionary<Guid, FormDefinition> forms;

    public void Erase(IEnumerable<BaseInfo> identities, IDictionary<string, object> configuration)
    {
        var contacts = identities.OfType<ContactInfo>().ToList();

        if (!contacts.Any())
        {
            return;
        }

        var contactIds = contacts.Select(c => c.ContactID).ToList();
        var contactEmails = contacts.Select(c => c.ContactEmail).ToList();

        using (new CMSActionContext())
        {
            DeleteSubmittedFormsActivities(contactIds, configuration);
            DeleteActivities(contactIds, configuration);
            DeleteContacts(contacts, configuration);
            DeleteSiteSubmittedFormsData(contactEmails, contactIds, configuration);
        }
    }

    private void DeleteSubmittedFormsActivities(ICollection<int> contactIds,
        IDictionary<string, object> configuration)
    {
        if (configuration.TryGetValue("DeleteSubmittedFormsActivities", out var deleteSubmittedFormsActivities)
            && ValidationHelper.GetBoolean(deleteSubmittedFormsActivities, false))
        {
            ActivityInfoProvider.ProviderObject.BulkDelete(new WhereCondition()
                .WhereEquals("ActivityType", PredefinedActivityType.BIZFORM_SUBMIT)
                .WhereIn("ActivityContactID", contactIds));
        }
    }

    private void DeleteSiteSubmittedFormsData(ICollection<string> emails, ICollection<int> contactIDs,
        IDictionary<string, object> configuration)
    {
        if (configuration.TryGetValue("DeleteSubmittedFormsData", out var deleteSubmittedForms)
            && ValidationHelper.GetBoolean(deleteSubmittedForms, false))
        {
            var consentAgreementGuids = ConsentAgreementInfo.Provider.Get()
                .Columns("ConsentAgreementGuid")
                .WhereIn("ConsentAgreementContactID", contactIDs);

            var formClasses = BizFormInfo.Provider.Get()
                .Source(s => s.LeftJoin<DataClassInfo>("CMS_Form.FormClassID", "ClassID"))
                .WhereIn("FormGUID", forms.Select(pair => pair.Key).ToList());

            formClasses.ForEachRow(row =>
            {
                var bizForm = new BizFormInfo(row);
                var formDefinition = forms[bizForm.FormGUID];

                var bizFormItems = formCollectionService.GetBizFormItems(emails, consentAgreementGuids, row, formDefinition);

                foreach (var bizFormItem in bizFormItems)
                {
                    bizFormItem.Delete();
                }
            });
        }
    }

    private void DeleteActivities(List<int> contactIds, IDictionary<string, object> configuration)
    {
        if (configuration.TryGetValue("DeleteActivities", out object deleteActivities)
            && ValidationHelper.GetBoolean(deleteActivities, false))
        {
            ActivityInfoProvider.ProviderObject.BulkDelete(
                new WhereCondition().WhereIn("ActivityContactID", contactIds));
        }
    }

    private static void DeleteContacts(IEnumerable<ContactInfo> contacts, IDictionary<string, object> configuration)
    {
        if (configuration.TryGetValue("DeleteContacts", out object deleteContacts) &&
            ValidationHelper.GetBoolean(deleteContacts, false))
        {
            foreach (ContactInfo contactInfo in contacts.Where(contact => contact.ContactID > 0))
            {
                ContactInfo.Provider.Delete(contactInfo);
            }
        }
    }

    public ContactDataEraser(IFormCollectionService formCollectionService)
    {
        this.formCollectionService = formCollectionService;
        forms = this.formCollectionService.GetForms();
    }
}

Keep in mind that your eraser needs to clear or anonymize all object types that store or reference visitor data in any way to comply with data protection laws. This example does not cover any custom logic specific to your solution.

Register the eraser

To complete this example, register the data eraser so that it is invoked by the system on the Right to be forgotten tab of the Data protection application in Xperience.

Open the DataProtectionRegistrationModule.cs file, and add the following line to the OnInit method.

This code can be placed anywhere within the scope of the method, though it may be best to put it after the existing code from the previous guides in order to mirror the ordering of the data protection UI and the order in which the data eraser is called in relation to the identity collector.

DataProtectionRegistrationModule.cs


...
// Adds the ContactDataEraser to the collection of registered personal data erasers
PersonalDataEraserRegister.Instance.Add(ActivatorUtilities.CreateInstance<ContactDataEraser>(serviceProvider));
...

Now, you can open the Data protection application from the Configuration category of the administration interface. On the Right to be forgotten tab, you can enter the email of a known contact and choose which data to delete, and Xperience will use the ContactDataEraser to remove their information from the system, as shown in the video below: