Track history of subscribe/unsubscribe and proof of consent

Created on 24 February 2019, almost 6 years ago
Updated 1 October 2023, about 1 year ago

Problem/Motivation

Sites are likely to need subscription history in case of disputes about whether a user was legitimately subscribed with their consent. In some countries this is required by law, including GDPR in EU.

This module already tracks some history for each subscription: when it was last changed and the "source" (which roughly indicates who/how it was changed). However this information is not really adequate for the purpose:

  1. Only the initial subscription is tracked. There is no information about any later unsubscribes or resubscribes.
  2. There is very limited information about "source" of the subscription (who did it and by what means) - mostly it contains a value "website" - see 📌 Improve the recording of "source" of subscriptions Closed: outdated
  3. The history information is not available in the GUI - see Create GUI for subscription history Active .

Proposed resolution

Option A: Make new database table

  1. Create a new table that stores past subscriptions, with fields similar to the columns of SubscriptionItem.
  2. Create entries in the new table in Subscriber::postSave(), by comparing with the ->original. The "source" can be calculated based on the current route.
  3. Remove SubscriptionItem and instead use EntityReferenceItem. SubscriptionItem no longer needs the 'status', 'timestamp' and 'source' properties so it is redundant, identical to EntityReferenceItem.
  4. The Subscriber::isUnsubscribed() status can be calculated with an SQL query on the new table.
  5. Remove all references to $source. Change Subscriber::subscribe() and unsubscribe() to take just a single $newsletter_id parameter.
  6. Remove all special case handling of the subscription field in forms.
  7. SubscriptionManager->[un]subscribe() become a simple wrapper to the subscriber functions that should only be used in cases where the code doesn't have a Subscriber.

Advantages:

  • Simplifies the existing code and makes it easier to understand by removing special-cases.
  • Code relating to subscription history is localised. This means that most of the codebase can ignore it, and it gives potential to integrate other consent modules. The history calculation provides the perfect place to fire hooks, fixing 🐛 hook_simplenews_subscribe fires before subscription Active .
  • The subscription history is independent of the Subscriber object, so could easily remain after that object is deleted. This could be used to support "right to be deleted", we could use an index based on a hash of the email value to prevent re-subscription without have to store the original email address.

Option B: Use revisions

  1. Create a new entity revision for each change in subscription status. See this guide . This automatically tracks author, time and log message of an unlimited number of past changes. We can add another field to store the source URL.
  2. Remove SubscriptionItem and instead use EntityReferenceItem. SubscriptionItem no longer needs the 'status', 'timestamp' and 'source' properties so it is redundant, identical to EntityReferenceItem.
  3. The isUnsubscribed() status can be calculated with an entity query on past revisions.
  4. SubscriptionManager->[un]subscribe() become a simple wrapper to saveNewRevision() that should only be used in cases where the code doesn't have a Subscriber. The signature is almost correct - change $source to be $log_message. They must not be called more than once for the same subscriber because that leads to unnecessary revisions.
  5. Key new function SubscriberInterface::saveNewRevision() (see below). Change subscribe() and unsubscribe() to take just a single $newsletter_id parameter.
  /**
   * Saves the subscriber tracking a new revision.
   *
   * @param string $log_message
   *   (optional) Log message for the new revision. Note that the URI, time and
   *   uid are already saved, so only set a message for something extra.
   * @param bool $save
   *   (optional) Whether to save the subscriber. Pass FALSE if the calling
   *   code will save, for example in form code.
   */
public function saveNewRevision(string $log_message = NULL, bool $save = TRUE);

Disadvantages: quite complex and likely to increase database usage significantly.

Option C: use SubscriptionItem

Create a new SubscriptionItem for every subscription and unsubscription so it keeps expanding. The outdated ones can have different status values like SIMPLENEWS_SUBSCRIPTION_STATUS_PREVIOUSLY_SUBSCRIBED.

This is the cheapest fix, however it's a bit hacky and unintuitive.

Related issues

Integrate with an existing consent module, in particular gdpr_consent: there is currently a sandbox project .

📌 Task
Status

Fixed

Version

4.0

Component

Code

Created by

🇬🇧United Kingdom adamps

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

Production build 0.71.5 2024