Note:

If you want to create a new page for developers, you should create it on the Moodle Developer Resource site.

Privacy API: Difference between revisions

From MoodleDocs
(see also, Category:GDPR)
(content moved from GDPR for plugin developers)
Line 1: Line 1:
{{Template:Work in progress}}
The General Data Protection Regulation (GDPR) is an EU directive that looks at providing users with more control over their data and how it is processed. This regulation will come into effect on 25th of May 2018 and covers any citizen or permanent resident of the European Union. The directive will be respected by a number of other countries outside of the European Union.
==Overview==
The Privacy API has been implemented to help report the use of data within Moodle Plugins as part of General Data Protection Regulations, and will help admins comply with key user rights such as:
* request information on the types of personal data held, instances of such data and retention policy for each
* their right to access their data ("Right of Access") *and* to have their data in a machine readable format ("Right to data portability")
* deletion of personal data ("Right to erasure" / "right to be forgotten")


It shall be expected that *all* plugins will require to report their use of and storing of personal data as part of their inclusion in the Moodle Plugins Database.
To help institutions become compliant with this new regulation we are adding functionality to Moodle. This includes a number of components, amongst others these include a user’s right to:


==Architecture==
* request information on the types of personal data held, the instances of that data, and the deletion policy for each;
The Privacy API comprises 3 components within the ''core_privacy'' subsystem:
* access all of their data; and
;Metadata providers
* be forgotten.
: A set of PHP interfaces to be implemented by components for that component to describe the kind of data that it stores, and the purpose for its storage
;Request providers
:  A set of PHP interfaces to be implemented by components to allow that component to act upon user requests such as the Right to be Forgotten, and a Subject Access Request
;Manager
: A concrete class used to bridge components which implement the providers with tools which request their data.


All plugins will be expected to implement a privacy provider class. This class will provide details on personal data being held by the plugin and where it is held, and may implement methods to allow the export or deletion of user data.
The compliance requirements also extend to installed plugins (including third party plugins). These need to also be able to report what information they store or process regarding users, and have the ability to provide and delete data for a user request.


==Plugins that do not hold personal data==
This document describes the proposed API changes required for plugins which will allow a Moodle installation to become GDPR compliant.
If a plugin does not hold any personal data it should implement a provider class that implements the ''core_privacy\local\metadata\null_provider''.


Some examples of plugin types which might fit this criteria include themes, blocks, filters, editor plugins, etc, however plugins which cause data to be stored elsewhere in Moodle (e.g. via a subsystem call) ''are'' considered to store data.
Target Audience: The intended audience for this document is Moodle plugin developers, who are aiming to ensure their plugins are updated to comply with GDPR requirements coming into effect in the EU in May, 2018.


===get_reason(): string===
This function should return a string that explains why the plugin is asserting that it holds no personal data.


===Example===
==Personal data in Moodle==
 
From the GDPR Spec, Article 4:
 
''‘personal data’ means any information relating to an identified or identifiable natural person (‘data subject’); an identifiable natural person is one who can be identified, directly or indirectly, in particular by reference to an identifier such as a name, an identification number, location data, an online identifier or to one or more factors specific to the physical, physiological, genetic, mental, economic, cultural or social identity of that natural person;''
 
In Moodle, we need to consider two main types of personal data; information entered by the user and information stored about the user. The key difference being that information stored about the user will have come from a source other than the user themselves. Both types of data can be used to form a profile of the individual.
 
The most obvious clue to finding personal data entered by the user is the presence of a userid on a database field. Any data on the record (or linked records) pertaining to that user may be deemed personal data for that user, including things like timestamps and record identification numbers. Additionally, any free text field which allows the user to enter information must also be considered to be the personal data of that user.
 
Data stored about the user includes things like ratings and comments made on a student submission. These may have been made by an assessor or teacher, but are considered the personal data of the student, as they are considered a reflection of the user’s competency in the subject matter and can be used to form a profile of that individual.
 
The sections that follow outline what you need to do as a plugin developer to ensure any personal data is advertised and can be accessed and deleted according to the GDPR requirements.
 
==Background==
 
===Architecture overview===
 
A new system for Privacy has been created within Moodle. This is broken down into several main parts and forms the ''core_privacy'' subsystem:
 
* Some metadata providers - a set of PHP interfaces to be implemented by components for that component to describe the kind of data that it stores, and the purpose for its storage;
* Some request providers - a set of PHP interfaces to be implemented by components to allow that component to act upon user requests such as the Right to be Forgotten, and a Subject Access Request; and
* A manager - a concrete class used to bridge components which implement the providers with tools which request their data.
 
All plugins will implement one metadata provider, and zero, one or two request providers.
 
The fetching of data is broken into two separate steps:
 
# Detecting in which Moodle contexts the user has any data; and
# Exporting all data from each of those contexts.
 
This has been broken into two steps to later allow administrators to exclude certain contexts from an export - e.g. for courses currently in progress.
 
A third component will later be added to facilitate the deletion of data within these contexts which will help to satisfy the Right to be Forgotten. This will also use the first step.
 
===Implementing a provider===
 
All plugins will need to create a concrete class which implements the relevant metadata and request providers. The exact providers you need to implement will depend on what data you store, and the type of plugin. This is covered in more detail in the following sections of the document.
 
In order to do so:
 
# You must create a class called ''provider'' within the namespace ''\your_pluginname\privacy''.
# This class must be created at ''path/to/your/plugin/classes/privacy/provider.php''.
# You must have your class implement the relevant metadata and request interfaces.
 
==Plugins which do not store personal data==
 
Many Moodle plugins do not store any personal data. This is usually the case for plugins which just add functionality, or which display the data already stored elsewhere in Moodle.
 
Some examples of plugin types which might fit this criteria include themes, blocks, filters, editor plugins, etc.
 
Plugins which cause data to be stored elsewhere in Moodle (e.g. via a subsystem call) are considered to store data.
 
One examples of a plugin which does not store any data would be the Calendar month block which just displays a view of the user’s calendar. It does not store any data itself.
One examples of a plugin which does not store any data would be the Calendar month block which just displays a view of the user’s calendar. It does not store any data itself.
An example of a plugin which must not use the null provider is the Comments block. The comments block is responsible for data subsequently being stored within Moodle. Although the block doesn’t store anything itself, it interacts with the comments subsystem and is the only component which knows how that data maps to a user.
===Implementation requirements===
In order to let Moodle know that you have audited your plugin, and that you do not store any personal user data, you must implement the ''\core_privacy\local\metadata\null_provider'' interface in your plugin’s provider.
The ''null_provider'' requires you to define one function ''get_reason()'' which returns the language string identifier within your component.
====Example====


''block/calendar_month/classes/privacy/provider.php''
''block/calendar_month/classes/privacy/provider.php''
<code php>
<code php>
<?php
// …
// …


Line 53: Line 104:
</code>
</code>


''block/calendar_month/lang/en/block_calendar_month,php''
''block/calendar_month/lang/en/block_calendar_month.php''
 
<code php>
<code php>
<?php
$string['privacy:null_reason'] = 'The calendar month block displays information from the Calendar, but does not effect or store any data itself. All changes are made via the Calendar.';
$string['privacy:null_reason'] = 'The calendar month block displays information from the Calendar, but does not effect or store any data itself. All changes are made via the Calendar.';
</code>
</code>


==Using Privacy API==
That’s it. Congratulations, your plugin now implements the Privacy API.
Every plugin should define a privacy provider class. At minimum this will describe the data held in the plugin, and implement means for servicing Subject Access Requests (SARs) and Deletion of User Data.
 
==Plugins which store personal data==
 
Many Moodle plugins do store some form of personal data.
 
In some cases this will be stored within database tables in your plugin, and in other cases this will be in one of Moodle’s core subsystems - for example your plugin may store files, ratings, comments, or tags.
 
Plugins which do store data will need to:
 
* Describe the type of data that they store;
* Provide a way to export that data; and
* Provide a way to delete that data.
 
Data is described via a ''metadata'' provider, and it is both exported and deleted via an implementation of a ''request'' provider.
 
These are both explained in the sections below.
 
===Describing the type of data you store===
 
In order to describe the type of data that you store, you must implement the ''\core_privacy\local\metadata\provider'' interface.
 
This interfaces requires that you define one function: ''get_metadata''.
 
There are several types of item to describe the data that you store. These are for:
 
* Items in the Moodle database;
* Items stored by you in a Moodle subsystem - for example files, and ratings; and
* User preferences stored site-wide within Moodle for your plugin
 
Note: All fields should include a description from a language string within your plugin.
 
====Example====
 
''mod/forum/classes/privacy/provider.php''
 
<code php>
<?php
// …
 
namespace mod_forum\privacy;
use \core_privacy\local\metadata\collection;
 
class provider implements
    # This plugin does store personal user data.
    \core_privacy\local\metadata\provider
{
    public static function get_metadata(collection $collection) : collection {
        return $collection;
    }
}
</code>
 
====Indicating that you store content in a Moodle subsystem====
 
Many plugins will use one of the core Moodle subsystems to store data.
 
As a plugin developer we do not expect you to describe those subsystems in detail, but we do need to know that you use them and to know what you use them for.
 
You can indicate this by calling the ''link_subsystem()'' method on the ''collection''.


Data is described via a ''metadata provider'', and it is both exported and deleted via an implementation of a ''request provider''.
=====Example=====


This ''must'':
''mod/forum/classes/privacy/provider.php''
* be named '''provider''',
* be in a '''<plugin>\classes\privacy\provider.php''' file.
* be in the "<pluginname>\privacy" namespace
* implement ''core_privacy\local\metadata\provider'' '''or''' ''core_privacy\local\metadata\null_provider''
* implement ''core_privacy\local\request\plugin\provider''  


<code php>
<code php>
namespace mod_peerassessment\privacy;
public static function get_metadata(collection $collection) : collection {
 
    $collection->link_subsystem(
        'core_files',
        'privacy:metadata:core_files'
    );


class provider implements
     return $collection;
     \core_privacy\local\metadata\provider,
    \core_privacy\local\request\plugin\provider
{
...
}
}
</code>
</code>
==Reporting Meta Data==
Moodle currently has 6 areas where data can be held:
* database tables
* user preferences
* subsystems
* external locations
* other Moodle plugins


A description for user data held in these areas is added to a metadata collection via ''add_<type>()'' or ''link_<type>()'' methods.
''mod/forum/lang/en/forum.php''
 
<code php>
<code php>
$collection->add_database_table($name, $fields, $summary);
<?php
$collection->add_user_preference($name, $summary);
 
$collection->link_subsystem($name, $summary);
$string['privacy:metadata:core_files'] = 'The forum stores files which have been uploaded by the user to form part of a forum post.';
$collection->link_external_location($name, $fields, $summary)
$collection->link_plugintype($name, $summary);
</code>
</code>


===get_metadata(collection $collection): collection===
====Describing data stored in database tables====
This function allows you describe the personal data that is held and where it is held in Moodle.
 
Most Moodle plugins will store some form of user data in their own database tables.


===Database tables===
As a plugin developer you will need to describe each database table, and each field which includes user data.
Most plugins will hold data in database tables. You should describe each database table and each field within that table ''that holds user data''.  


It is up to you to determine which fields constitute the "user data".
=====Example=====


====Example====
''mod/forum/classes/privacy/provider.php''
''mod/forum/classes/privacy/provider.php''
<code php>
<code php>
public static function get_metadata(collection $collection) : collection {
public static function get_metadata(collection $collection) : collection {
Line 126: Line 226:


''mod/forum/lang/en/forum.php''
''mod/forum/lang/en/forum.php''
<code php>
<code php>
<?php


$string['privacy:metadata:forum_discussion_subs'] = 'Information about the subscriptions to individual forum discussions. This includes when a user has chosen to subscribe to a discussion, or to unsubscribe from one where they would otherwise be subscribed.';
$string['privacy:metadata:forum_discussion_subs'] = 'Information about the subscriptions to individual forum discussions. This includes when a user has chosen to subscribe to a discussion, or to unsubscribe from one where they would otherwise be subscribed.';
Line 133: Line 235:
$string['privacy:metadata:forum_discussion_subs:preference'] = 'The start time of the subscription.';
$string['privacy:metadata:forum_discussion_subs:preference'] = 'The start time of the subscription.';
</code>
</code>
===Site-Wide User Preferences===
Each plugin should be aware of how it handles its own preferences.


Where there are multiple instances of a user-preference (e.g. ''tool_usertours_tour_completion_time_'''2''''') only the general name needs to be indicated.
====Indicating that you store site-wide user preferences====
 
Many plugins will include one or more user preferences. Unfortunately this is one of Moodle’s older components and many of the values stored are not pure user preferences. Each plugin should be aware of how it handles its own preferences and is best placed to determine whether they are site-wide preferences, or per-instance preferences.
 
Whilst most of these will have a fixed name (e.g. ''filepicker_recentrepository''), some will include a variable of some kind (e.g. ''tool_usertours_tour_completion_time_2''). Only the general name needs to be indicated rather than one copy for each preference.
 
Also, these should only be ''site-wide'' user preferences which do not belong to a specific Moodle context.
 
In the above examples:


Any plugin providing user preferences must also implement the '''core_privacy\local\request\user_preference_provider''' interface, and the  
* Preference ''filepicker_recentrepository'' belongs to the file subsystem, and is a site-wide preference affecting the user anywhere that they view the filepicker.
'''export_user_preferences(int $userid)''' function.
* Preference ''tool_usertours_tour_completion_time_2'' belongs to user tours. User tours are a site-wide feature which can affect many parts of Moodle and cross multiple contexts.


===Moodle Sub System===
In some cases a value may be stored in the preferences table but is known to belong to a specific context within Moodle. In these cases they should be stored as metadata against that context rather than as a site-wide user preference.
Your plugin may use one of Moodle's sub-systems to store data (tk Link to list of sub-systems).  


You do '''not''' have to describe the sub-system in detail, only the how your plugin uses it.
You can indicate this by calling the ''add_user_preference()'' method on the ''collection''.
 
Any plugin providing user preferences must also implement the ''\core_privacy\local\request\preference_provider''.
 
=====Example=====
 
''admin/tool/usertours/classes/privacy/provider.php''


====Example====
''mod/forum/classes/privacy/provider.php''
<code php>
<code php>
public static function get_metadata(collection $collection) : collection {
public static function get_metadata(collection $collection) : collection {


     $collection->link_subsystem(
     $collection->add_user_preference('tool_usertours_tour_completion_time,
        'core_files',
         'privacy:metadata:preference:tool_usertours_tour_completion_time');
         'privacy:metadata:core_files'
    );


     return $collection;
     return $collection;
Line 160: Line 269:
</code>
</code>


''mod/forum/lang/en/forum.php''
''admin/tool/usertours/lang/en/tool_usertours.php''
 
<code php>
<code php>
$string['privacy:metadata:core_files'] = 'The forum stores files which have been uploaded by the user to form part of a forum post.';
<?php
 
$string['privacy:metadata:tool_usertours_tour_completion_time'] = 'The time that a specific user tour was last completed by a user.';
</code>
</code>


===External Locations===
====Indicating that you export data to an external location====
 
Many plugins will interact with external systems - for example cloud-based services. Often this external location is configurable within the plugin either at the site or the instance level.
Many plugins will interact with external systems - for example cloud-based services. Often this external location is configurable within the plugin either at the site or the instance level.


You will need to describe each type of target destination, alongside a list of each exported field which includes user data.
As a plugin developer you will need to describe each ''type'' of target destination, alongside a list of each exported field which includes user data.
The ''actual'' destination does not need to be described as this can change based on configuration.
 
You can indicate this by calling the ''link_external_location()'' method on the collection.


The actual destination does not need to be described as this can change based on configuration.
=====Example=====


You can indicate this by calling the '''link_external_location()''' method on the collection.
====Example====
''mod/lti/classes/privacy/provider.php''
''mod/lti/classes/privacy/provider.php''
<code php>
<code php>
public static function get_metadata(collection $collection) : collection {
public static function get_metadata(collection $collection) : collection {
Line 188: Line 303:


''admin/tool/usertours/lang/en/tool_usertours.php''
''admin/tool/usertours/lang/en/tool_usertours.php''
<code php>
<code php>
<?php
$string['privacy:metadata:lti_client'] = 'In order to integrate with a remote LTI service, user data needs to be exchanged with that service.';
$string['privacy:metadata:lti_client'] = 'In order to integrate with a remote LTI service, user data needs to be exchanged with that service.';
$string['privacy:metadata:lti_client:userid'] = 'The userid is sent from Moodle to allow you to access your data on the remote system.';
$string['privacy:metadata:lti_client:userid'] = 'The userid is sent from Moodle to allow you to access your data on the remote system.';
Line 194: Line 312:
</code>
</code>


===Other Moodle plugins===
===Providing a way to export user data===
 
In order to export the user data that you store, you must implement the relevant request provider.
 
We have named these request providers because they are called in response to a specific request from a user to access their information.
 
There are several different types of request provider, and you may need to implement several of these, depending on the type and nature of your plugin.
 
Broadly speaking plugins will fit into one of the following categories:
 
* Plugins which are a subplugin of another plugin. Examples include ''assignsubmission'', ''atto'', and ''datafield'';
* Plugins which are typically called by a Moodle subsystem. Examples include ''qtype'', and ''profilefield'';
* All other plugins which store data.
 
Most plugins will fit into this final category, whilst other plugins may fall into several categories.
Plugins which ''define'' a subplugin will also be responsible for  collecting this data from their subplugins.
 
A final category exists - plugins which store user preferences. In some cases this may be the ''only'' provider implemented.
 
====Standard plugins which store data====
 
A majority of Moodle plugins will fit into this category and will be required to implement the ''\core_privacy\local\request\plugin\provider'' interface. This interface requires that you define two functions:
 
* ''get_contexts_for_userid'' - to explain where data is held within Moodle for your plugin; and
* ''export_user_data'' - to export a user’s personal data from your plugin.
 
These APIs make use of the Moodle ''context'' system to hierarchically store this data.
 
====Retrieving the list of contexts====
 
Contexts are retrieved using the ''get_contexts_for_userid'' function which takes the ID of the user being fetched, and returns a list of contexts in which the user has any data.
 
''mod/forum/classes/privacy/provider.php''
 
<code php>
    /**
    * Get the list of contexts that contain user information for the specified user.
    *
    * @param  int          $userid      The user to search.
    * @return  contextlist  $contextlist  The list of contexts used in this plugin.
    */
    public static function get_contexts_for_userid(int $userid) : contextlist {}
</code>
 
The function returns a ''\core_privacy\local\request\contextlist'' which is used to keep a set of contexts together in a fixed fashion.


==Exporting User Data==
Because a Subject Access Request covers ''every'' piece of data that is held for a user within Moodle, efficiency and performance is highly important. As a result, contexts are added to the ''contextlist'' by defining one or more SQL queries which return just the contextid. Multiple SQL queries can be added as required.
To support the export of user data for Subject Access Requests, the plugin '''provider''' class should implement '''\core_privacy\local\request\plugin\provider'''


There are 3 cases to be considered:
Many plugins will interact with specific subsystems and store data within them.
* Plugins that store user-preferences
These subsystems will also provide a way in which to link the data that you have stored with your own database tables.
* Plugins that '''define'' sub-plugins
At present these are still a work in progress and only the ''core_ratings'' subsystem includes this.
* Plugins that ''are'' sub-plugins


===Plugins that store user-preferences===
=====Basic example=====


===Plugins that '''define'' sub-plugins===
The following example simply fetches the contextid for all forums where a user has a single discussion (note: this is an incomplete example):
If your plugin defines sub-plugins you must define a contract between your plugin and any sub-plugins.


For each sub-plugin you define you must create a new interface that extends the '''\core_privacy\local'request\plugin\subplugin_provider''' interface.
''mod/forum/classes/privacy/provider.php''
 
<code php>
    /**
    * Get the list of contexts that contain user information for the specified user.
    *
    * @param  int          $userid      The user to search.
    * @return  contextlist  $contextlist  The list of contexts used in this plugin.
    */
    public static function get_contexts_for_userid(int $userid) : contextlist {
        $contextlist = new \core_privacy\local\request\contextlist();


For example, the Assignment plugin defines the '''assignsubmission_provider''' and '''assignfeedback_provider''' interfaces, which must be implemented by any Assignment Submission or Feedback plugins.  
        $sql = "SELECT c.id
                FROM {context} c
          INNER JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
          INNER JOIN {modules} m ON m.id = cm.module AND m.name = :modname
          INNER JOIN {forum} f ON f.id = cm.instance
            LEFT JOIN {forum_discussions} d ON d.forum = f.id
                WHERE (
                d.userid        = :discussionuserid
                )
        ";


===Plugins that ''are'' sub-plugins===
        $params = [
If your plugin is a 'sub-plugin' you will need to look at the parent plugin to identify what interface you need to implement.
            'modname'          => 'forum',
            'contextlevel'     => CONTEXT_MODULE,
            'discussionuserid' => $userid,
        ];


==Deleting User Data==
        $contextlist->add_from_sql($sql, $params);
    }
</code>


==Helpers==
=====More complete example=====
 
The following example includes a link to core_rating.
It will find any forum, forum discussion, or forum post where the user has any data, including:
 
* Per-forum digest preferences;
* Per-forum subscription preferences;
* Per-forum read tracking preferences;
* Per-discussion subscription preferences;
* Per-post read data (if a user has read a post or not); and
* Per-post rating data.
 
In the case of the rating data, this will include any post where the user has rated the post of another user.
 
''mod/forum/classes/privacy/provider.php''
 
<code php>
/**
* Get the list of contexts that contain user information for the specified user.
*
* @param  int          $userid      The user to search.
* @return  contextlist  $contextlist  The list of contexts used in this plugin.
*/
public static function get_contexts_for_userid(int $userid) : contextlist {
    $ratingsql = \core_rating\privacy\provider::get_sql_join('rat', 'mod_forum', 'post', 'p.id', $userid);
    // Fetch all forum discussions, and forum posts.
    $sql = "SELECT c.id
                FROM {context} c
        INNER JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
        INNER JOIN {modules} m ON m.id = cm.module AND m.name = :modname
        INNER JOIN {forum} f ON f.id = cm.instance
            LEFT JOIN {forum_discussions} d ON d.forum = f.id
            LEFT JOIN {forum_posts} p ON p.discussion = d.id
            LEFT JOIN {forum_digests} dig ON dig.forum = f.id
            LEFT JOIN {forum_subscriptions} sub ON sub.forum = f.id
            LEFT JOIN {forum_track_prefs} pref ON pref.forumid = f.id
            LEFT JOIN {forum_read} hasread ON hasread.forumid = f.id
            LEFT JOIN {forum_discussion_subs} dsub ON dsub.forum = f.id
            {$ratingsql->join}
                WHERE (
                p.userid        = :postuserid OR
                d.userid        = :discussionuserid OR
                dig.userid      = :digestuserid OR
                sub.userid      = :subuserid OR
                pref.userid    = :prefuserid OR
                hasread.userid  = :hasreaduserid OR
                dsub.userid    = :dsubuserid OR
                {$ratingsql->userwhere}
            )
    ";
 
    $params = [
        'modname'          => 'forum',
        'contextlevel'      => CONTEXT_MODULE,
        'postuserid'        => $userid,
        'discussionuserid'  => $userid,
        'digestuserid'      => $userid,
        'subuserid'        => $userid,
        'prefuserid'        => $userid,
        'hasreaduserid'    => $userid,
        'dsubuserid'        => $userid,
    ];
    $params += $ratingsql->params;
 
    $contextlist = new \core_privacy\local\request\contextlist();
    $contextlist->add_from_sql($sql, $params);
 
    return $contextlist;
 
 
}
</code>
 
====Exporting user data====
 
After determining where in Moodle your plugin holds data about a user, the ''\core_privacy\manager'' will then ask your plugin to export all user data for a subset of those locations.
 
This is achieved through use of the ''export_user_data'' function which takes the list of approved contexts in a ''\core_privacy\local\request\approved_contextlist'' object.
 
''mod/forum/classes/privacy/provider.php''
 
<code php>
/**
* Export all user data for the specified user, in the specified contexts, using the supplied exporter instance.
*
* @param  approved_contextlist    $contextlist    The approved contexts to export information for.
*/
public static function export_user_data(approved_contextlist $contextlist) {}
</code>
 
The ''approved_contextlist'' includes both the user record, and a list of contexts, which can be retrieved by either processing it as an Iterator, or by calling ''get_contextids()'' or ''get_contexts()'' as required.
 
Data is exported using a ''\core_privacy\local\request\content_writer'', which is described in further detail below.
 
===Plugins which store user preferences===
 
Many plugins store a variety of user preferences, and must therefore export them.
 
Since user preferences are a site-wide preference, these are exported separately to other user data.
In some cases the only data present is user preference data, whilst in others there is a combination of user-provided data, and user preferences.
 
Storing of user preferences is achieved through implementation of the ''\core_privacy\local\request\preference_provider'' interface which defines one required function -- ''export_user_preferences''.
 
====Example====
 
''mod/forum/classes/privacy/provider.php''
 
<code php>
/**
* Export all user preferences for the plugin.
*
* @param  int        $userid The userid of the user whose data is to be exported.
*/
public static function export_user_preferences(int $userid) {
    $markasreadonnotification = get_user_preference('markasreadonnotification', null, $userid);
    if (null !== $markasreadonnotification) {
        switch ($markasreadonnotification) {
            case 0:
                $markasreadonnotificationdescription = get_string('markasreadonnotificationno', 'mod_forum');
                break;
            case 1:
            default:
                $markasreadonnotificationdescription = get_string('markasreadonnotificationyes', 'mod_forum');
                break;
        }
        writer::export_user_preference('mod_forum', 'markasreadonnotification', $markasreadonnotification, $markasreadonnotificationdescription);
    }
}
</code>
 
===Plugins which define a subplugin===
 
Many plugin types are also able to define their own subplugins and will need to define a contract between themselves and their subplugins in order to fetch their data.
 
This is required as the parent plugin and the child subplugin should be separate entities and the parent plugin must be able to function if one or more of its subplugins are uninstalled.
 
The parent plugin is responsible for defining the contract,  and for interacting with its subplugins, though we intend to create helpers to make this easier.
 
The parent plugin should define a new interface for each type of subplugin that it defines. This interface should extend the ''\core_privacy\local\request\plugin\subplugin_provider'' interface.
 
====Example====
 
The following example defines the contract that assign submission subplugins may be required to implement.
 
The assignment module is responsible for returning the contexts of all assignments where a user has data, but in some cases it is unaware of all of those cases - for example if a Teacher comments on a student submission it may not be aware of these as the information about this interaction may not be stored within its own tables.
 
''mod/assign/privacy/assignsubmission_provider.php''
 
<code php>
<?php
// …
 
namespace mod_assign\privacy;
use \core_privacy\local\metadata\collection;
 
 
interface assignsubmission_provider extends
    # This Interface defines a subplugin.
    \core_privacy\local\request\subplugin_provider
{
 
    /**
    * Get the SQL required to find all submission items where this user has had any involvements.
    *
    * @param  int          $userid      The user to search.
    * @return  \stdClass                  Object containing the join, params, and where used to select a these records from the database.
    */
    public static function get_items_with_user_interaction(int $userid) : \stdClass ;
 
    /**
    * Export all relevant user submissions information which match the combination of userid and attemptid.
    *
    * @param  int          $userid      The user to search.
    * @param  \context      $context      The context to export this submission against.
    * @param  array        $subcontext  The subcontext within the context to export this information
    * @param  int          $attid        The id of the submission to export.
    */
    public static function export_user_submissions(int $userid, \context $context, array $subcontext, int $attid) ;
 
}
</code>
 
===Plugins which are subplugins to another plugin===
 
If you are developing a sub-plugin of another plugin, then you will have to look at the relevant plugin in order to determine the exact contract.
 
Each subplugin type should define a new interface which extends the ''\core_privacy\local\request\plugin\subplugin_provider'' interface and it is up to the parent plugin to define how they will interact with their children.
 
The principles remain the same, but the exact implementation will differ depending upon requirements.
 
''mod/pluginname/classes/privacy/provider.php''
 
<code php>
<?php
// …
namespace assignsubmission\onlinetext;
 
class provider implements
    # This plugin does store personal user data.
    \core_privacy\local\metadata\provider,
 
    # This plugin is a subplugin of assign and must meet that contract.
    \mod_assign\privacy\assignsubmission_provider
{
}
</code>
 
===Plugins which are typically called by a Moodle subsystem===
 
There are a number of plugintypes in Moodle which are typically called by a specific Moodle subsystem.
 
Some of these are ''only'' called by that subsystem, for example plugins which are of the ''plagiarism'' plugintype should never be called directly, but are always invoked via the ''core_plagiarism'' subsystem.
 
Conversely, there maybe other plugintypes which can be called both via a subsystem, and in some other fashion. We are still determining whether any plugintypes currently fit this pattern.
 
If you are developing a plugin which belongs to a specific subsystem, then you will have to look at the relevant plugin in order to determine the exact contract.
 
Each subsystem will define a new interface which extends the ''\core_privacy\local\request\plugin\subsystem_provider'' interface and it is up to that subsystem to define how they will interact with those plugins.
 
The principles remain the same, but the exact implementation will differ depending upon requirements.
 
''plagiarism/detectorator/classes/privacy/provider.php''
 
<code php>
<?php
// …
namespace plagiarism_detectorator\privacy;
 
class provider implements
    # This plugin does export personal user data.
    \core_privacy\local\metadata\provider,
 
    # This plugin is always linked against another activity module via the Plagiarism API.
    \core_plagiarism\privacy\plugin_provider
{
}
</code>
 
===Exporting data===
 
Any plugin which stores data must also export it.
 
To cater for this the privacy API includes a ''\core_privacy\local\request\content_writer'', which defines a set of functions to store different types of data.
 
Broadly speaking data is broken into the following types:
 
* Data - this is the object being described. For example the post content in a forum post;
* Related data - this is data related to the object being stored. For example, ratings of a forum post;
* Metadata - This is metadata about the main object. For example whether you are subscribed to a forum discussion;
* User preferences - this is data about a site-wide preference;
* Files - Any files that you are stored within Moodle on behalf of this plugin; and
* Custom files - For custom file formats - e.g. a calendar feed for calendar data. These should be used sparingly.
 
Each piece of data is stored against a specific Moodle ''context'', which will define how the data is structured within the exporter.
Data, and Related data only accept the ''stdClass'' object, whilst metadata should be stored as a set of key/value pairs which include a description.
 
In some cases the data being stored belongs within an implicit structure. For example, One forum has many forum discussions, which each have a number of forum posts. This structure is represented by an ''array'' referred to as a ''subcontext''.
 
The ''content_writer'' must ''always'' be called with a specific context, and can be called as follows:
 
''mod/pluginname/classes/privacy/provider.php''
 
<code php>
<?php
// …
use \core_privacy\local\request\writer;


==Pointers==
* If a field supports Moodle files it must be re-written:
<code php>use \core_privacy\local\request\writer;
writer::with_context($context)
writer::with_context($context)
     ->rewrite_pluginfile_urls($postarea, 'mod_forum', 'post', $post->id, $post->message);
     ->export_data($subcontext, $post)
    ->export_area_files($subcontext, 'mod_forum', 'post', $post->id)
    ->export_metadata($subcontext, 'postread', (object) ['firstread' => $firstread], new \lang_string('privacy:export:post:postread'));
<code>
 
''mod/pluginname/classes/privacy/provider.php''
 
<code php>
<?php
// …
use \core_privacy\local\request\writer;
 
writer::with_context($context)
    ->export_data($subcontext, $post)
    ->export_area_files($subcontext, 'mod_forum', 'post', $post->id)
    ->export_metadata($subcontext, 'postread', (object) ['firstread' => $firstread], new \lang_string('privacy:export:post:postread'));
</code>
 
===Providing a way to delete user data===
 
Deleting user data is also implemented in the request interface. There are two methods that need to be created. The first one to remove all user data from a context, the other to remove user data for a specific user in a list of contexts.
 
====Delete for a context====
 
A context is given and all user data (for all users) is to be deleted from the plugin. This will be called when the retention period for the plugin has expired to adhere to the privacy by design requirement.�
 
''mod/choice/classes/privacy/provider.php''
 
<code php>
public static function delete_data_for_all_users_in_context(deletion_criteria $criteria) {
    global $DB;
    $context = $criteria->get_context();
    if (empty($context)) {
        return;
    }
    $instanceid = $DB->get_field('course_modules', 'instance', ['id' => $context->instanceid], MUST_EXIST);
    $DB->delete_records('choice_answers', ['choiceid' => $instanceid]);
}
</code>
 
====Delete personal information for a specific user and context(s)====
 
An ''approved_contextlist'' is given and user data related to that user should either be completely deleted, or overwritten if a structure needs to be maintained. This will be called when a user has requested the right to be forgotten. All attempts should be made to delete this data where practical while still allowing the plugin to be used by other users.
 
''mod/choice/classes/privacy/provider.php''
 
<code php>
public static function delete_data_for_user(approved_contextlist $contextlist) {
    global $DB;
   
    if (empty($contextlist->count())) {
        return;
    }
    $userid = $contextlist->get_user()->id;
    foreach ($contextlist->get_contexts() as $context) {
        $instanceid = $DB->get_field('course_modules', 'instance', ['id' => $context->instanceid], MUST_EXIST);
        $DB->delete_records('choice_answers', ['choiceid' => $instanceid, 'userid' => $userid]);
    }
}
</code>
</code>


==See also==
==See also==


* [[GDPR for plugin developers]]
* [[Subject Access Request FAQ]]
* [[:en:GDPR|GDPR]] in the user documentation


[[Category:GDPR]]
[[Category:GDPR]]

Revision as of 05:20, 28 March 2018

The General Data Protection Regulation (GDPR) is an EU directive that looks at providing users with more control over their data and how it is processed. This regulation will come into effect on 25th of May 2018 and covers any citizen or permanent resident of the European Union. The directive will be respected by a number of other countries outside of the European Union.

To help institutions become compliant with this new regulation we are adding functionality to Moodle. This includes a number of components, amongst others these include a user’s right to:

  • request information on the types of personal data held, the instances of that data, and the deletion policy for each;
  • access all of their data; and
  • be forgotten.

The compliance requirements also extend to installed plugins (including third party plugins). These need to also be able to report what information they store or process regarding users, and have the ability to provide and delete data for a user request.

This document describes the proposed API changes required for plugins which will allow a Moodle installation to become GDPR compliant.

Target Audience: The intended audience for this document is Moodle plugin developers, who are aiming to ensure their plugins are updated to comply with GDPR requirements coming into effect in the EU in May, 2018.


Personal data in Moodle

From the GDPR Spec, Article 4:

‘personal data’ means any information relating to an identified or identifiable natural person (‘data subject’); an identifiable natural person is one who can be identified, directly or indirectly, in particular by reference to an identifier such as a name, an identification number, location data, an online identifier or to one or more factors specific to the physical, physiological, genetic, mental, economic, cultural or social identity of that natural person;

In Moodle, we need to consider two main types of personal data; information entered by the user and information stored about the user. The key difference being that information stored about the user will have come from a source other than the user themselves. Both types of data can be used to form a profile of the individual.

The most obvious clue to finding personal data entered by the user is the presence of a userid on a database field. Any data on the record (or linked records) pertaining to that user may be deemed personal data for that user, including things like timestamps and record identification numbers. Additionally, any free text field which allows the user to enter information must also be considered to be the personal data of that user.

Data stored about the user includes things like ratings and comments made on a student submission. These may have been made by an assessor or teacher, but are considered the personal data of the student, as they are considered a reflection of the user’s competency in the subject matter and can be used to form a profile of that individual.

The sections that follow outline what you need to do as a plugin developer to ensure any personal data is advertised and can be accessed and deleted according to the GDPR requirements.

Background

Architecture overview

A new system for Privacy has been created within Moodle. This is broken down into several main parts and forms the core_privacy subsystem:

  • Some metadata providers - a set of PHP interfaces to be implemented by components for that component to describe the kind of data that it stores, and the purpose for its storage;
  • Some request providers - a set of PHP interfaces to be implemented by components to allow that component to act upon user requests such as the Right to be Forgotten, and a Subject Access Request; and
  • A manager - a concrete class used to bridge components which implement the providers with tools which request their data.

All plugins will implement one metadata provider, and zero, one or two request providers.

The fetching of data is broken into two separate steps:

  1. Detecting in which Moodle contexts the user has any data; and
  2. Exporting all data from each of those contexts.

This has been broken into two steps to later allow administrators to exclude certain contexts from an export - e.g. for courses currently in progress.

A third component will later be added to facilitate the deletion of data within these contexts which will help to satisfy the Right to be Forgotten. This will also use the first step.

Implementing a provider

All plugins will need to create a concrete class which implements the relevant metadata and request providers. The exact providers you need to implement will depend on what data you store, and the type of plugin. This is covered in more detail in the following sections of the document.

In order to do so:

  1. You must create a class called provider within the namespace \your_pluginname\privacy.
  2. This class must be created at path/to/your/plugin/classes/privacy/provider.php.
  3. You must have your class implement the relevant metadata and request interfaces.

Plugins which do not store personal data

Many Moodle plugins do not store any personal data. This is usually the case for plugins which just add functionality, or which display the data already stored elsewhere in Moodle.

Some examples of plugin types which might fit this criteria include themes, blocks, filters, editor plugins, etc.

Plugins which cause data to be stored elsewhere in Moodle (e.g. via a subsystem call) are considered to store data.

One examples of a plugin which does not store any data would be the Calendar month block which just displays a view of the user’s calendar. It does not store any data itself.

An example of a plugin which must not use the null provider is the Comments block. The comments block is responsible for data subsequently being stored within Moodle. Although the block doesn’t store anything itself, it interacts with the comments subsystem and is the only component which knows how that data maps to a user.

Implementation requirements

In order to let Moodle know that you have audited your plugin, and that you do not store any personal user data, you must implement the \core_privacy\local\metadata\null_provider interface in your plugin’s provider.

The null_provider requires you to define one function get_reason() which returns the language string identifier within your component.

Example

block/calendar_month/classes/privacy/provider.php

<?php

// …

namespace block_calendar_month\privacy;

class provider implements

   # This plugin does not store any personal user data.
   \core_privacy\local\metadata\null_provider

{

   /**
    * Get the language string identifier with the component's language
    * file to explain why this plugin stores no data.
    *
    * @return  string
    */
   public static function get_reason() : string {
       return 'privacy:null_reason';
   }

}

block/calendar_month/lang/en/block_calendar_month.php

<?php

$string['privacy:null_reason'] = 'The calendar month block displays information from the Calendar, but does not effect or store any data itself. All changes are made via the Calendar.';

That’s it. Congratulations, your plugin now implements the Privacy API.

Plugins which store personal data

Many Moodle plugins do store some form of personal data.

In some cases this will be stored within database tables in your plugin, and in other cases this will be in one of Moodle’s core subsystems - for example your plugin may store files, ratings, comments, or tags.

Plugins which do store data will need to:

  • Describe the type of data that they store;
  • Provide a way to export that data; and
  • Provide a way to delete that data.

Data is described via a metadata provider, and it is both exported and deleted via an implementation of a request provider.

These are both explained in the sections below.

Describing the type of data you store

In order to describe the type of data that you store, you must implement the \core_privacy\local\metadata\provider interface.

This interfaces requires that you define one function: get_metadata.

There are several types of item to describe the data that you store. These are for:

  • Items in the Moodle database;
  • Items stored by you in a Moodle subsystem - for example files, and ratings; and
  • User preferences stored site-wide within Moodle for your plugin

Note: All fields should include a description from a language string within your plugin.

Example

mod/forum/classes/privacy/provider.php

<?php // …

namespace mod_forum\privacy; use \core_privacy\local\metadata\collection;

class provider implements

   # This plugin does store personal user data.
   \core_privacy\local\metadata\provider

{

   public static function get_metadata(collection $collection) : collection {
       return $collection;
   }

}

Indicating that you store content in a Moodle subsystem

Many plugins will use one of the core Moodle subsystems to store data.

As a plugin developer we do not expect you to describe those subsystems in detail, but we do need to know that you use them and to know what you use them for.

You can indicate this by calling the link_subsystem() method on the collection.

Example

mod/forum/classes/privacy/provider.php

public static function get_metadata(collection $collection) : collection {

   $collection->link_subsystem(
       'core_files',
       'privacy:metadata:core_files'
   );
   return $collection;

}

mod/forum/lang/en/forum.php

<?php

$string['privacy:metadata:core_files'] = 'The forum stores files which have been uploaded by the user to form part of a forum post.';

Describing data stored in database tables

Most Moodle plugins will store some form of user data in their own database tables.

As a plugin developer you will need to describe each database table, and each field which includes user data.

Example

mod/forum/classes/privacy/provider.php

public static function get_metadata(collection $collection) : collection {

   $collection->add_database_table(
       'forum_discussion_subs',
        [
           'userid' => 'privacy:metadata:forum_discussion_subs:userid',
           'discussionid' => 'privacy:metadata:forum_discussion_subs:discussionid',
           'preference' => 'privacy:metadata:forum_discussion_subs:preference',
        ],
       'privacy:metadata:forum_discussion_subs'
   );
   return $collection;

}

mod/forum/lang/en/forum.php

<?php

$string['privacy:metadata:forum_discussion_subs'] = 'Information about the subscriptions to individual forum discussions. This includes when a user has chosen to subscribe to a discussion, or to unsubscribe from one where they would otherwise be subscribed.'; $string['privacy:metadata:forum_discussion_subs:userid'] = 'The ID of the user with this subscription preference.'; $string['privacy:metadata:forum_discussion_subs:discussionid'] = 'The ID of the discussion that was subscribed to.'; $string['privacy:metadata:forum_discussion_subs:preference'] = 'The start time of the subscription.';

Indicating that you store site-wide user preferences

Many plugins will include one or more user preferences. Unfortunately this is one of Moodle’s older components and many of the values stored are not pure user preferences. Each plugin should be aware of how it handles its own preferences and is best placed to determine whether they are site-wide preferences, or per-instance preferences.

Whilst most of these will have a fixed name (e.g. filepicker_recentrepository), some will include a variable of some kind (e.g. tool_usertours_tour_completion_time_2). Only the general name needs to be indicated rather than one copy for each preference.

Also, these should only be site-wide user preferences which do not belong to a specific Moodle context.

In the above examples:

  • Preference filepicker_recentrepository belongs to the file subsystem, and is a site-wide preference affecting the user anywhere that they view the filepicker.
  • Preference tool_usertours_tour_completion_time_2 belongs to user tours. User tours are a site-wide feature which can affect many parts of Moodle and cross multiple contexts.

In some cases a value may be stored in the preferences table but is known to belong to a specific context within Moodle. In these cases they should be stored as metadata against that context rather than as a site-wide user preference.

You can indicate this by calling the add_user_preference() method on the collection.

Any plugin providing user preferences must also implement the \core_privacy\local\request\preference_provider.

Example

admin/tool/usertours/classes/privacy/provider.php

public static function get_metadata(collection $collection) : collection {

   $collection->add_user_preference('tool_usertours_tour_completion_time,
       'privacy:metadata:preference:tool_usertours_tour_completion_time');
   return $collection;

}

admin/tool/usertours/lang/en/tool_usertours.php

<?php

$string['privacy:metadata:tool_usertours_tour_completion_time'] = 'The time that a specific user tour was last completed by a user.';

Indicating that you export data to an external location

Many plugins will interact with external systems - for example cloud-based services. Often this external location is configurable within the plugin either at the site or the instance level.

As a plugin developer you will need to describe each type of target destination, alongside a list of each exported field which includes user data. The actual destination does not need to be described as this can change based on configuration.

You can indicate this by calling the link_external_location() method on the collection.

Example

mod/lti/classes/privacy/provider.php

public static function get_metadata(collection $collection) : collection {

   $collection->link_external_location('lti_client', [
           'userid' => 'privacy:metadata:lti_client:userid',
           'fullname' => 'privacy:metadata:lti_client:fullname',
       ], 'privacy:metadata:lti_client');
   return $collection;

}

admin/tool/usertours/lang/en/tool_usertours.php

<?php

$string['privacy:metadata:lti_client'] = 'In order to integrate with a remote LTI service, user data needs to be exchanged with that service.'; $string['privacy:metadata:lti_client:userid'] = 'The userid is sent from Moodle to allow you to access your data on the remote system.'; $string['privacy:metadata:lti_client:fullname'] = 'Your full name is sent to the remote system to allow a better user experience.';

Providing a way to export user data

In order to export the user data that you store, you must implement the relevant request provider.

We have named these request providers because they are called in response to a specific request from a user to access their information.

There are several different types of request provider, and you may need to implement several of these, depending on the type and nature of your plugin.

Broadly speaking plugins will fit into one of the following categories:

  • Plugins which are a subplugin of another plugin. Examples include assignsubmission, atto, and datafield;
  • Plugins which are typically called by a Moodle subsystem. Examples include qtype, and profilefield;
  • All other plugins which store data.

Most plugins will fit into this final category, whilst other plugins may fall into several categories. Plugins which define a subplugin will also be responsible for collecting this data from their subplugins.

A final category exists - plugins which store user preferences. In some cases this may be the only provider implemented.

Standard plugins which store data

A majority of Moodle plugins will fit into this category and will be required to implement the \core_privacy\local\request\plugin\provider interface. This interface requires that you define two functions:

  • get_contexts_for_userid - to explain where data is held within Moodle for your plugin; and
  • export_user_data - to export a user’s personal data from your plugin.

These APIs make use of the Moodle context system to hierarchically store this data.

Retrieving the list of contexts

Contexts are retrieved using the get_contexts_for_userid function which takes the ID of the user being fetched, and returns a list of contexts in which the user has any data.

mod/forum/classes/privacy/provider.php

   /**
    * Get the list of contexts that contain user information for the specified user.
    *
    * @param   int           $userid       The user to search.
    * @return  contextlist   $contextlist  The list of contexts used in this plugin.
    */
   public static function get_contexts_for_userid(int $userid) : contextlist {}

The function returns a \core_privacy\local\request\contextlist which is used to keep a set of contexts together in a fixed fashion.

Because a Subject Access Request covers every piece of data that is held for a user within Moodle, efficiency and performance is highly important. As a result, contexts are added to the contextlist by defining one or more SQL queries which return just the contextid. Multiple SQL queries can be added as required.

Many plugins will interact with specific subsystems and store data within them. These subsystems will also provide a way in which to link the data that you have stored with your own database tables. At present these are still a work in progress and only the core_ratings subsystem includes this.

Basic example

The following example simply fetches the contextid for all forums where a user has a single discussion (note: this is an incomplete example):

mod/forum/classes/privacy/provider.php

   /**
    * Get the list of contexts that contain user information for the specified user.
    *
    * @param   int           $userid       The user to search.
    * @return  contextlist   $contextlist  The list of contexts used in this plugin.
    */
   public static function get_contexts_for_userid(int $userid) : contextlist {
       $contextlist = new \core_privacy\local\request\contextlist();
       $sql = "SELECT c.id
                FROM {context} c
          INNER JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
          INNER JOIN {modules} m ON m.id = cm.module AND m.name = :modname
          INNER JOIN {forum} f ON f.id = cm.instance
           LEFT JOIN {forum_discussions} d ON d.forum = f.id
               WHERE (
               d.userid        = :discussionuserid
               )
       ";
       $params = [
           'modname'           => 'forum',
           'contextlevel'      => CONTEXT_MODULE,
           'discussionuserid'  => $userid,
       ];
       $contextlist->add_from_sql($sql, $params);
   }

More complete example

The following example includes a link to core_rating. It will find any forum, forum discussion, or forum post where the user has any data, including:

  • Per-forum digest preferences;
  • Per-forum subscription preferences;
  • Per-forum read tracking preferences;
  • Per-discussion subscription preferences;
  • Per-post read data (if a user has read a post or not); and
  • Per-post rating data.

In the case of the rating data, this will include any post where the user has rated the post of another user.

mod/forum/classes/privacy/provider.php

/**

* Get the list of contexts that contain user information for the specified user.
*
* @param   int           $userid       The user to search.
* @return  contextlist   $contextlist  The list of contexts used in this plugin.
*/

public static function get_contexts_for_userid(int $userid) : contextlist {

   $ratingsql = \core_rating\privacy\provider::get_sql_join('rat', 'mod_forum', 'post', 'p.id', $userid);
   // Fetch all forum discussions, and forum posts.
   $sql = "SELECT c.id
               FROM {context} c
       INNER JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
       INNER JOIN {modules} m ON m.id = cm.module AND m.name = :modname
       INNER JOIN {forum} f ON f.id = cm.instance
           LEFT JOIN {forum_discussions} d ON d.forum = f.id
           LEFT JOIN {forum_posts} p ON p.discussion = d.id
           LEFT JOIN {forum_digests} dig ON dig.forum = f.id
           LEFT JOIN {forum_subscriptions} sub ON sub.forum = f.id
           LEFT JOIN {forum_track_prefs} pref ON pref.forumid = f.id
           LEFT JOIN {forum_read} hasread ON hasread.forumid = f.id
           LEFT JOIN {forum_discussion_subs} dsub ON dsub.forum = f.id
           {$ratingsql->join}
               WHERE (
               p.userid        = :postuserid OR
               d.userid        = :discussionuserid OR
               dig.userid      = :digestuserid OR
               sub.userid      = :subuserid OR
               pref.userid     = :prefuserid OR
               hasread.userid  = :hasreaduserid OR
               dsub.userid     = :dsubuserid OR
               {$ratingsql->userwhere}
           )
   ";
   $params = [
       'modname'           => 'forum',
       'contextlevel'      => CONTEXT_MODULE,
       'postuserid'        => $userid,
       'discussionuserid'  => $userid,
       'digestuserid'      => $userid,
       'subuserid'         => $userid,
       'prefuserid'        => $userid,
       'hasreaduserid'     => $userid,
       'dsubuserid'        => $userid,
   ];
   $params += $ratingsql->params;
   $contextlist = new \core_privacy\local\request\contextlist();
   $contextlist->add_from_sql($sql, $params);
   return $contextlist;


}

Exporting user data

After determining where in Moodle your plugin holds data about a user, the \core_privacy\manager will then ask your plugin to export all user data for a subset of those locations.

This is achieved through use of the export_user_data function which takes the list of approved contexts in a \core_privacy\local\request\approved_contextlist object.

mod/forum/classes/privacy/provider.php

/**

* Export all user data for the specified user, in the specified contexts, using the supplied exporter instance.
*
* @param   approved_contextlist    $contextlist    The approved contexts to export information for.
*/

public static function export_user_data(approved_contextlist $contextlist) {}

The approved_contextlist includes both the user record, and a list of contexts, which can be retrieved by either processing it as an Iterator, or by calling get_contextids() or get_contexts() as required.

Data is exported using a \core_privacy\local\request\content_writer, which is described in further detail below.

Plugins which store user preferences

Many plugins store a variety of user preferences, and must therefore export them.

Since user preferences are a site-wide preference, these are exported separately to other user data. In some cases the only data present is user preference data, whilst in others there is a combination of user-provided data, and user preferences.

Storing of user preferences is achieved through implementation of the \core_privacy\local\request\preference_provider interface which defines one required function -- export_user_preferences.

Example

mod/forum/classes/privacy/provider.php

/**

* Export all user preferences for the plugin.
*
* @param   int         $userid The userid of the user whose data is to be exported.
*/

public static function export_user_preferences(int $userid) {

   $markasreadonnotification = get_user_preference('markasreadonnotification', null, $userid);
   if (null !== $markasreadonnotification) {
       switch ($markasreadonnotification) {
           case 0:
               $markasreadonnotificationdescription = get_string('markasreadonnotificationno', 'mod_forum');
               break;
           case 1:
           default:
               $markasreadonnotificationdescription = get_string('markasreadonnotificationyes', 'mod_forum');
               break;
       }
       writer::export_user_preference('mod_forum', 'markasreadonnotification', $markasreadonnotification, $markasreadonnotificationdescription);
   }

}

Plugins which define a subplugin

Many plugin types are also able to define their own subplugins and will need to define a contract between themselves and their subplugins in order to fetch their data.

This is required as the parent plugin and the child subplugin should be separate entities and the parent plugin must be able to function if one or more of its subplugins are uninstalled.

The parent plugin is responsible for defining the contract, and for interacting with its subplugins, though we intend to create helpers to make this easier.

The parent plugin should define a new interface for each type of subplugin that it defines. This interface should extend the \core_privacy\local\request\plugin\subplugin_provider interface.

Example

The following example defines the contract that assign submission subplugins may be required to implement.

The assignment module is responsible for returning the contexts of all assignments where a user has data, but in some cases it is unaware of all of those cases - for example if a Teacher comments on a student submission it may not be aware of these as the information about this interaction may not be stored within its own tables.

mod/assign/privacy/assignsubmission_provider.php

<?php // …

namespace mod_assign\privacy; use \core_privacy\local\metadata\collection;


interface assignsubmission_provider extends

   # This Interface defines a subplugin.
   \core_privacy\local\request\subplugin_provider

{

   /**
    * Get the SQL required to find all submission items where this user has had any involvements. 
    *
    * @param   int           $userid       The user to search.
    * @return  \stdClass                   Object containing the join, params, and where used to select a these records from the database.
    */
   public static function get_items_with_user_interaction(int $userid) : \stdClass ;
   /**
    * Export all relevant user submissions information which match the combination of userid and attemptid.
    *
    * @param   int           $userid       The user to search.
    * @param   \context      $context      The context to export this submission against.
    * @param   array         $subcontext   The subcontext within the context to export this information
    * @param   int           $attid        The id of the submission to export.
    */
   public static function export_user_submissions(int $userid, \context $context, array $subcontext, int $attid) ;

}

Plugins which are subplugins to another plugin

If you are developing a sub-plugin of another plugin, then you will have to look at the relevant plugin in order to determine the exact contract.

Each subplugin type should define a new interface which extends the \core_privacy\local\request\plugin\subplugin_provider interface and it is up to the parent plugin to define how they will interact with their children.

The principles remain the same, but the exact implementation will differ depending upon requirements.

mod/pluginname/classes/privacy/provider.php

<?php // … namespace assignsubmission\onlinetext;

class provider implements

   # This plugin does store personal user data.
   \core_privacy\local\metadata\provider,
   # This plugin is a subplugin of assign and must meet that contract.
   \mod_assign\privacy\assignsubmission_provider

{ }

Plugins which are typically called by a Moodle subsystem

There are a number of plugintypes in Moodle which are typically called by a specific Moodle subsystem.

Some of these are only called by that subsystem, for example plugins which are of the plagiarism plugintype should never be called directly, but are always invoked via the core_plagiarism subsystem.

Conversely, there maybe other plugintypes which can be called both via a subsystem, and in some other fashion. We are still determining whether any plugintypes currently fit this pattern.

If you are developing a plugin which belongs to a specific subsystem, then you will have to look at the relevant plugin in order to determine the exact contract.

Each subsystem will define a new interface which extends the \core_privacy\local\request\plugin\subsystem_provider interface and it is up to that subsystem to define how they will interact with those plugins.

The principles remain the same, but the exact implementation will differ depending upon requirements.

plagiarism/detectorator/classes/privacy/provider.php

<?php // … namespace plagiarism_detectorator\privacy;

class provider implements

   # This plugin does export personal user data.
   \core_privacy\local\metadata\provider,
   # This plugin is always linked against another activity module via the Plagiarism API.
   \core_plagiarism\privacy\plugin_provider

{ }

Exporting data

Any plugin which stores data must also export it.

To cater for this the privacy API includes a \core_privacy\local\request\content_writer, which defines a set of functions to store different types of data.

Broadly speaking data is broken into the following types:

  • Data - this is the object being described. For example the post content in a forum post;
  • Related data - this is data related to the object being stored. For example, ratings of a forum post;
  • Metadata - This is metadata about the main object. For example whether you are subscribed to a forum discussion;
  • User preferences - this is data about a site-wide preference;
  • Files - Any files that you are stored within Moodle on behalf of this plugin; and
  • Custom files - For custom file formats - e.g. a calendar feed for calendar data. These should be used sparingly.

Each piece of data is stored against a specific Moodle context, which will define how the data is structured within the exporter. Data, and Related data only accept the stdClass object, whilst metadata should be stored as a set of key/value pairs which include a description.

In some cases the data being stored belongs within an implicit structure. For example, One forum has many forum discussions, which each have a number of forum posts. This structure is represented by an array referred to as a subcontext.

The content_writer must always be called with a specific context, and can be called as follows:

mod/pluginname/classes/privacy/provider.php

<?php // … use \core_privacy\local\request\writer;

writer::with_context($context)

   ->export_data($subcontext, $post)
   ->export_area_files($subcontext, 'mod_forum', 'post', $post->id)
   ->export_metadata($subcontext, 'postread', (object) ['firstread' => $firstread], new \lang_string('privacy:export:post:postread'));

mod/pluginname/classes/privacy/provider.php

<?php // … use \core_privacy\local\request\writer;

writer::with_context($context)

   ->export_data($subcontext, $post)
   ->export_area_files($subcontext, 'mod_forum', 'post', $post->id)
   ->export_metadata($subcontext, 'postread', (object) ['firstread' => $firstread], new \lang_string('privacy:export:post:postread'));

Providing a way to delete user data

Deleting user data is also implemented in the request interface. There are two methods that need to be created. The first one to remove all user data from a context, the other to remove user data for a specific user in a list of contexts.

Delete for a context

A context is given and all user data (for all users) is to be deleted from the plugin. This will be called when the retention period for the plugin has expired to adhere to the privacy by design requirement.�

mod/choice/classes/privacy/provider.php

public static function delete_data_for_all_users_in_context(deletion_criteria $criteria) {

   global $DB;
   $context = $criteria->get_context();
   if (empty($context)) {
       return;
   }
   $instanceid = $DB->get_field('course_modules', 'instance', ['id' => $context->instanceid], MUST_EXIST);
   $DB->delete_records('choice_answers', ['choiceid' => $instanceid]);

}

Delete personal information for a specific user and context(s)

An approved_contextlist is given and user data related to that user should either be completely deleted, or overwritten if a structure needs to be maintained. This will be called when a user has requested the right to be forgotten. All attempts should be made to delete this data where practical while still allowing the plugin to be used by other users.

mod/choice/classes/privacy/provider.php

public static function delete_data_for_user(approved_contextlist $contextlist) {

   global $DB;
   
   if (empty($contextlist->count())) {
       return;
   }
   $userid = $contextlist->get_user()->id;
   foreach ($contextlist->get_contexts() as $context) {
       $instanceid = $DB->get_field('course_modules', 'instance', ['id' => $context->instanceid], MUST_EXIST);
       $DB->delete_records('choice_answers', ['choiceid' => $instanceid, 'userid' => $userid]);
   }

}

See also