J3.x

J3.x:Integrate Extensions with the Privacy Component

From Joomla! Documentation

Other languages:
Deutsch • ‎English • ‎Nederlands • ‎français • ‎italiano

The information below is provided to extension developers to help create integrations with the Privacy Tool Suite.

Privacy Related Extension Capabilities

The new privacy component has a Capabilities section which allows extensions to report privacy related capabilities. The aim for this section is to help users to understand what an extension may do related to personal data on users and help site owners plan accordingly. For details on this section, including how a plugin should integrate with it, please see the Report Extension Capabilities in Privacy Component page.

Check for a Published Privacy Policy

The privacy component's health check notifies users if there is a privacy policy published on the website. This check is performed by the onPrivacyCheckPrivacyPolicyPublished event which can be subscribed to by plugins in the privacy, system, and user plugin groups. The event receives an associative array by reference with two keys as its argument:

  • "published" - A boolean value indicating that there is a published policy
  • "editLink" - The URL to edit the policy item, this is displayed if there is a privacy policy published
  • "articlePublished" - The article state when it exists in the database

As a best practice, it is suggested that plugins first check if the "published" flag is already set to true and not make further changes to the data array if so. Note that the System - Privacy Consent core plugin will process this event.

public function onPrivacyCheckPrivacyPolicyPublished(&$policy)
{
	// If another plugin has already indicated a policy is published, we won't change anything here
	if ($policy['published'])
	{
		return;
	}

	// Do stuff to find the privacy policy data

	// For core, we check if the article exists in the database and is published or not
	$query = $this->db->getQuery(true)
		->select($this->db->quoteName(array('id', 'state')))
		->from($this->db->quoteName('#__content'))
		->where($this->db->quoteName('id') . ' = ' . (int) $articleId);
	$this->db->setQuery($query);

	$article = $this->db->loadObject();

	// Check if the article exists
	if (!$article)
	{
		return;
	}

	// Check if the article is published
	if ($article->state == 1)
	{
		$policy['articlePublished'] = true;
	}

	$policy['published'] = true;
	$policy['editLink']  = ''; // The link to the item's edit page, processed through JRoute, i.e. JRoute::_('index.php?option=com_content&task=article.edit&id=1');
}


Add Data to Export Requests

As a convenience, there are several helper methods in the PrivacyPlugin class and it is recommended that privacy plugins extend this class to inherit those helpers (this is similar to the FinderIndexerAdapter class for Smart Search plugins as an example).

JLoader::register('PrivacyPlugin', JPATH_ADMINISTRATOR . '/components/com_privacy/helpers/plugin.php');

class PlgPrivacyContent extends PrivacyPlugin {}

To add data to an export request, a plugin in the privacy or system plugin groups must subscribe to the onPrivacyExportRequest event.

The event receives two arguments:

  • $request - A PrivacyTableRequest object containing the information request record from the database
  • $user - If there is an account for the email address of the information request, a Joomla\CMS\User\User object is given containing the user account data

The event must return an array of PrivacyExportDomain objects containing the data to be exported for a given domain. If the plugin has no data, it must return an empty array.

A PrivacyExportDomain data object typically represents the data found from a table in the database and is made up of three elements:

  • Domain name - A name to identify the domain
  • Domain description - A short description of the data contained in the domain
  • Domain items - An array of PrivacyExportItem data objects containing all items within the domain

A PrivacyExportItem data object represents a single item within the data domain and is made up of two elements:

  • Item ID - The primary identifier for this item within the domain, typically this will be the database record's primary key
  • Item fields - An array of PrivacyExportField data objects containing each field for the item

The PrivacyExportField data object represents a single field within an item and is made up of two elements:

  • Field name - The name of the field
  • Field value - The field's value

The general workflow for the export process is this:

  • Validate the plugin should actually process data
  • Query the data from the database
  • Create a domain for the results (the createDomain method inherited from the PrivacyPlugin class can help with this)
  • Add items for each row
    • The createItemFromArray method inherited from the PrivacyPlugin class can be used to create a PrivacyExportItem object from an array (this should be used in conjunction with the database's loadAssocList method)
    • The createItemForTable method inherited from the PrivacyPlugin class can be used to create a PrivacyExportItem object from a Joomla\CMS\Table\Table object
  • Return the domain

Below is an example for exporting articles created by a user, including custom field data.

use Joomla\CMS\User\User;

JLoader::register('FieldsHelper', JPATH_ADMINISTRATOR . '/components/com_fields/helpers/fields.php');
JLoader::register('PrivacyPlugin', JPATH_ADMINISTRATOR . '/components/com_privacy/helpers/plugin.php');

class PlgPrivacyContent extends PrivacyPlugin
{
	/**
	 * @var  JDatabaseDriver
	 */
	protected $db;

	/**
	 * @var  array
	 */
	protected $contents = array();

	public function onPrivacyExportRequest(PrivacyTableRequest $request, User $user = null)
	{
		// This plugin only processes data for registered user accounts
		if (!$user)
		{
			return array();
		}

		$domains   = array();
		$domains[] = $this->createContentDomain($user);

		// Create domains for each article's custom fields
		foreach ($this->contents as $content)
		{
			$domains[] = $this->createContentCustomFieldsDomain($content);
		}

		return $domains;
	}

	private function createContentDomain(User $user)
	{
		$domain = $this->createDomain('user_content', 'joomla_user_content_data');

		$query = $this->db->getQuery(true)
			->select('*')
			->from($this->db->quoteName('#__content'))
			->where($this->db->quoteName('created_by') . ' = ' . (int) $user->id)
			->order($this->db->quoteName('ordering') . ' ASC');

		$items = $this->db->setQuery($query)->loadAssocList();

		// Add each article to the domain
		foreach ($items as $item)
		{
			$domain->addItem($this->createItemFromArray($item));

			// Store the article for use in the custom fields processing
			$this->contents[] = (object) $item;
		}

		return $domain;
	}

	private function createContentCustomFieldsDomain($content)
	{
		$domain = $this->createDomain('content_custom_fields', 'joomla_content_custom_fields_data');

		// Get item's fields, also preparing their value property for manual display
		$fields = FieldsHelper::getFields('com_content.article', $content);

		foreach ($fields as $field)
		{
			$fieldValue = is_array($field->value) ? implode(', ', $field->value) : $field->value;

			$data = array(
				'content_id'  => $content->id,
				'field_name'  => $field->name,
				'field_title' => $field->title,
				'field_value' => $fieldValue,
			);

			$domain->addItem($this->createItemFromArray($data));
		}

		return $domain;
	}
}


Process Removal Requests

Processing a removal request requires two steps: Validating that the subject's data can be removed, and the actual removal. Again it is suggested that plugins extend the PrivacyPlugin class, however this is not a strict requirement.

JLoader::register('PrivacyPlugin', JPATH_ADMINISTRATOR . '/components/com_privacy/helpers/plugin.php');

class PlgPrivacyUser extends PrivacyPlugin {}

Validate Data Can Be Removed

If the plugin may need to block a subject's data from being removed, it must subscribe to the onPrivacyCanRemoveData event. Generally, a removal should be blocked if there are valid legal reasons to retain the data or if removal may cause permanent damage to the site (for example, the core Privacy - User Accounts prohibits removing a super user account).

The event receives two arguments:

  • $request - A PrivacyTableRequest object containing the information request record from the database
  • $user - If there is an account for the email address of the information request, a Joomla\CMS\User\User object is given containing the user account data

The event must return a PrivacyRemovalStatus data object which specifies if the data cannot be removed and a reason for the inability to remove the data.

use Joomla\CMS\Language\Text;
use Joomla\CMS\User\User;

JLoader::register('PrivacyPlugin', JPATH_ADMINISTRATOR . '/components/com_privacy/helpers/plugin.php');
JLoader::register('PrivacyRemovalStatus', JPATH_ADMINISTRATOR . '/components/com_privacy/helpers/removal/status.php');

class PlgPrivacyUser extends PrivacyPlugin
{
	public function onPrivacyCanRemoveData(PrivacyTableRequest $request, User $user = null)
	{
		$status = new PrivacyRemovalStatus;

		// We only need to check if there is an associated user account
		if (!$user)
		{
			return $status;
		}

		// We will not remove a super user account from the site
		if ($user->authorise('core.admin'))
		{
			$status->canRemove = false;
			$status->reason    = Text::_('PLG_PRIVACY_USER_ERROR_CANNOT_REMOVE_SUPER_USER');
		}

		return $status;
	}
}

Remove or Anonymize Data

If the plugin handles the removal or anonymization of data, it must subscribe to the onPrivacyRemoveData event.

The event receives two arguments:

  • $request - A PrivacyTableRequest object containing the information request record from the database
  • $user - If there is an account for the email address of the information request, a Joomla\CMS\User\User object is given containing the user account data

Unlike the other events in this component, plugins are not expected to return any data.

Depending on the underlying data's requirements, plugins should anonymize information that is retained and remove any data that is no longer necessary. If necessary, the plugin may perform other actions related to the request and/or user account.

The below example demonstrates how Joomla anonymizes and removes data related to the core user account.

use Joomla\CMS\Factory;
use Joomla\CMS\User\User;

JLoader::register('PrivacyPlugin', JPATH_ADMINISTRATOR . '/components/com_privacy/helpers/plugin.php');

class PlgPrivacyUser extends PrivacyPlugin
{
	/**
	 * @var  JDatabaseDriver
	 */
	protected $db;

	public function onPrivacyRemoveData(PrivacyTableRequest $request, User $user = null)
	{
		// This plugin only processes data for registered user accounts
		if (!$user)
		{
			return;
		}

		$pseudoanonymisedData = array(
			'name'      => 'User ID ' . $user->id,
			'username'  => bin2hex(random_bytes(12)), // Generates a random username
			'email'     => 'UserID' . $user->id . 'removed@email.invalid',
			'block'     => true,
		);

		$user->bind($pseudoanonymisedData);

		$user->save();

		// Destroy all sessions for the user account
		$sessionIds = $this->db->setQuery(
			$this->db->getQuery(true)
				->select($this->db->quoteName('session_id'))
				->from($this->db->quoteName('#__session'))
				->where($this->db->quoteName('userid') . ' = ' . (int) $user->id)
		)->loadColumn();

		// If there aren't any active sessions then there's nothing to do here
		if (empty($sessionIds))
		{
			return;
		}

		$storeName = Factory::getConfig()->get('session_handler', 'none');
		$store     = JSessionStorage::getInstance($storeName);
		$quotedIds = array();

		// Destroy the sessions and quote the IDs to purge the session table
		foreach ($sessionIds as $sessionId)
		{
			$store->destroy($sessionId);
			$quotedIds[] = $this->db->quote($sessionId);
		}

		$this->db->setQuery(
			$this->db->getQuery(true)
				->delete($this->db->quoteName('#__session'))
				->where($this->db->quoteName('session_id') . ' IN (' . implode(', ', $quotedIds) . ')')
		)->execute();
	}
}