diff --git a/privacy/classes/local/metadata/collection.php b/privacy/classes/local/metadata/collection.php new file mode 100644 index 00000000000..f82a92d50ee --- /dev/null +++ b/privacy/classes/local/metadata/collection.php @@ -0,0 +1,160 @@ +. + +/** + * This file defines the core_privacy\local\metadata\collection class object. + * + * The collection class is used to organize a collection of types + * objects, which contains the privacy field details of a component. + * + * @package core_privacy + * @copyright 2018 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_privacy\local\metadata; + +use core_privacy\local\metadata\types\type; + +defined('MOODLE_INTERNAL') || die(); + +/** + * A collection of metadata items. + * + * @copyright 2018 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class collection { + + /** + * @var string The component that the items in the collection belong to. + */ + protected $component; + + /** + * @var array The collection of metadata items. + */ + protected $collection = []; + + /** + * Constructor for a component's privacy collection class. + * + * @param string $component component name. + */ + public function __construct($component) { + $this->component = $component; + } + + /** + * Function to add an object that implements type interface to the current collection. + * + * @param type $type to add to collection. + * @return $this + */ + public function add_type(type $type) { + $this->collection[] = $type; + + return $this; + } + + /** + * Function to add a database table which contains user data to this collection. + * + * @param string $name the name of the database table. + * @param array $privacyfields An associative array of fieldname to description. + * @param string $summary A description of what the table is used for. + * @return $this + */ + public function add_database_table($name, array $privacyfields, $summary = '') { + $this->add_type(new types\database_table($name, $privacyfields, $summary)); + + return $this; + } + + /** + * Function to link a subsystem to the component. + * + * @param string $name the name of the subsystem to link. + * @param string $summary A description of what is stored within this subsystem. + * @return $this + */ + public function link_subsystem($name, $summary = '') { + $this->add_type(new types\subsystem_link($name, $summary)); + + return $this; + } + + /** + * Function to link a plugin to the component. + * + * @param string $name the name of the plugin to link. + * @param string $summary A description of what tis stored within this plugin. + * @return $this + */ + public function link_plugintype($name, $summary = '') { + $this->add_type(new types\plugintype_link($name, $summary)); + + return $this; + } + + /** + * Function to indicate that data may be exported to an external location. + * + * @param string $name A name for the type of data exported. + * @param array $privacyfields A list of fields with their description. + * @param string $summary A description of what the table is used for. This is a language string identifier + * within the component. + * @return $this + */ + public function link_external_location($name, array $privacyfields, $summary = '') { + $this->add_type(new types\external_location($name, $privacyfields, $summary)); + + return $this; + } + + /** + * Add a type of user preference to the collection. + * + * Typically this is a single user preference, but in some cases the + * name of a user preference fits a particular format. + * + * @param string $name The name of the user preference. + * @param string $summary A description of what the preference is used for. + * @return $this + */ + public function add_user_preference($name, $summary = '') { + $this->add_type(new types\user_preference($name, $summary)); + + return $this; + } + + /** + * Function to return the current component name. + * + * @return string + */ + public function get_component() { + return $this->component; + } + + /** + * The content of this collection. + * + * @return types\type[] + */ + public function get_collection() { + return $this->collection; + } +} diff --git a/privacy/classes/local/metadata/null_provider.php b/privacy/classes/local/metadata/null_provider.php new file mode 100644 index 00000000000..e57a57f8320 --- /dev/null +++ b/privacy/classes/local/metadata/null_provider.php @@ -0,0 +1,40 @@ +. + +/** + * This file contains the core_privacy\nodata interface. + * + * Plugins implement this interface to declare that they don't store any personal information. + * + * @package core_privacy + * @copyright 2018 Jake Dallimore + * + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_privacy\local\metadata; + +defined('MOODLE_INTERNAL') || die(); + +interface 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 ; +} diff --git a/privacy/classes/local/metadata/provider.php b/privacy/classes/local/metadata/provider.php new file mode 100644 index 00000000000..21060b7add8 --- /dev/null +++ b/privacy/classes/local/metadata/provider.php @@ -0,0 +1,43 @@ +. + +/** + * INterface for main metadata provider interface. + * + * @package core_privacy + * @copyright 2018 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_privacy\local\metadata; + +defined('MOODLE_INTERNAL') || die(); + +/** + * INterface for main metadata provider interface. + * + * @copyright 2018 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +interface provider { + + /** + * Returns meta data about this system. + * + * @param collection $collection The initialised collection to add items to. + * @return collection A listing of user data stored through this system. + */ + public static function get_metadata(collection $collection) : collection ; +} diff --git a/privacy/classes/local/metadata/types/database_table.php b/privacy/classes/local/metadata/types/database_table.php new file mode 100644 index 00000000000..e7b99f4806e --- /dev/null +++ b/privacy/classes/local/metadata/types/database_table.php @@ -0,0 +1,110 @@ +. + +/** + * This file defines an item of metadata which encapsulates a database table. + * + * @package core_privacy + * @copyright 2018 Zig Tan + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_privacy\local\metadata\types; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The database_table type. + * + * @copyright 2018 Zig Tan + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class database_table implements type { + + /** + * @var string Database table name. + */ + protected $name; + + /** + * @var array Fields which contain user information within the table. + */ + protected $privacyfields; + + /** + * @var string A description of what this table is used for. + */ + protected $summary; + + /** + * Constructor to create a new database_table type. + * + * @param string $name The name of the database table being described. + * @param array $privacyfields A list of fields with their description. + * @param string $summary A description of what the table is used for. + */ + public function __construct($name, array $privacyfields = [], $summary = '') { + if (debugging('', DEBUG_DEVELOPER)) { + if (empty($privacyfields)) { + debugging("Table '{$name}' was supplied without any fields.", DEBUG_DEVELOPER); + } + + foreach ($privacyfields as $key => $field) { + $teststring = clean_param($field, PARAM_STRINGID); + if ($teststring !== $field) { + debugging("Field '{$key}' passed for table '{$name}' has an invalid langstring identifier: '{$field}'", + DEBUG_DEVELOPER); + } + } + + $teststring = clean_param($summary, PARAM_STRINGID); + if ($teststring !== $summary) { + debugging("Summary information for the '{$name}' table has an invalid langstring identifier: '{$summary}'", + DEBUG_DEVELOPER); + } + } + + $this->name = $name; + $this->privacyfields = $privacyfields; + $this->summary = $summary; + } + + /** + * The name of the database table. + * + * @return string + */ + public function get_name() { + return $this->name; + } + + /** + * The list of fields within the table which contain user data, with a description of each field. + * + * @return array + */ + public function get_privacy_fields() { + return $this->privacyfields; + } + + /** + * A summary of what this table is used for. + * + * @return string + */ + public function get_summary() { + return $this->summary; + } +} diff --git a/privacy/classes/local/metadata/types/external_location.php b/privacy/classes/local/metadata/types/external_location.php new file mode 100644 index 00000000000..df9db9f8cfb --- /dev/null +++ b/privacy/classes/local/metadata/types/external_location.php @@ -0,0 +1,112 @@ +. + +/** + * This file defines an item of metadata which encapsulates data which is exported to an external location. + * + * @package core_privacy + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_privacy\local\metadata\types; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The external_location type. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class external_location implements type { + + /** + * @var string The name to describe the type of information exported. + */ + protected $name; + + /** + * @var array The list of data names and descriptions exported. + */ + protected $privacyfields; + + /** + * @var string A description of what this table is used for. + * This is a language string identifier. + */ + protected $summary; + + /** + * Constructor to create a new external_location type. + * + * @param string $name A name for the type of data exported. + * @param array $privacyfields A list of fields with their description. + * @param string $summary A description of what the table is used for. This is a language string identifier + * within the component. + */ + public function __construct($name, array $privacyfields = [], $summary = '') { + if (debugging('', DEBUG_DEVELOPER)) { + if (empty($privacyfields)) { + debugging("Location '{$name}' was supplied without any fields.", DEBUG_DEVELOPER); + } + + foreach ($privacyfields as $key => $field) { + $teststring = clean_param($field, PARAM_STRINGID); + if ($teststring !== $field) { + debugging("Field '{$key}' passed for location '{$name}' has an invalid langstring identifier: '{$field}'", + DEBUG_DEVELOPER); + } + } + + $teststring = clean_param($summary, PARAM_STRINGID); + if ($teststring !== $summary) { + debugging("Summary information for the '{$name}' location has an invalid langstring identifier: '{$summary}'", + DEBUG_DEVELOPER); + } + } + + $this->name = $name; + $this->privacyfields = $privacyfields; + $this->summary = $summary; + } + + /** + * The name to describe the type of information exported. + * + * @return string + */ + public function get_name() { + return $this->name; + } + + /** + * Get the list of fields which contain user data, with a description of each field. + * + * @return array + */ + public function get_privacy_fields() { + return $this->privacyfields; + } + + /** + * A summary of what this type of exported data is used for. + * + * @return string + */ + public function get_summary() { + return $this->summary; + } +} diff --git a/privacy/classes/local/metadata/types/plugintype_link.php b/privacy/classes/local/metadata/types/plugintype_link.php new file mode 100644 index 00000000000..54b29cf3ee1 --- /dev/null +++ b/privacy/classes/local/metadata/types/plugintype_link.php @@ -0,0 +1,92 @@ +. + +/** + * This file defines a link to another Moodle plugin. + * + * @package core_privacy + * @copyright 2018 Zig Tan + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_privacy\local\metadata\types; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The plugintype link. + * + * @copyright 2018 Zig Tan + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class plugintype_link implements type { + + /** + * @var The name of the core plugintype to link. + */ + protected $name; + + /** + * @var string A description of what this plugintype is used to store. + */ + protected $summary; + + /** + * Constructor for the plugintype_link. + * + * @param string $name The name of the plugintype to link. + * @param string $summary A description of what is stored within this plugintype. + */ + public function __construct($name, $summary = '') { + if (debugging('', DEBUG_DEVELOPER)) { + $teststring = clean_param($summary, PARAM_STRINGID); + if ($teststring !== $summary) { + debugging("Summary information for use of the '{$name}' plugintype " . + "has an invalid langstring identifier: '{$summary}'", + DEBUG_DEVELOPER); + } + } + + $this->name = $name; + $this->summary = $summary; + } + + /** + * Function to return the name of this plugintype_link type. + * + * @return string $name + */ + public function get_name() { + return $this->name; + } + + /** + * A plugintype link does not define any fields itself. + * + * @return array + */ + public function get_privacy_fields() : array { + return null; + } + + /** + * A summary of what this plugintype is used for. + * + * @return string $summary + */ + public function get_summary() { + return $this->summary; + } +} diff --git a/privacy/classes/local/metadata/types/subsystem_link.php b/privacy/classes/local/metadata/types/subsystem_link.php new file mode 100644 index 00000000000..88eedb57d2d --- /dev/null +++ b/privacy/classes/local/metadata/types/subsystem_link.php @@ -0,0 +1,92 @@ +. + +/** + * This file defines a link to another Moodle subsystem. + * + * @package core_privacy + * @copyright 2018 Zig Tan + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_privacy\local\metadata\types; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The subsystem link type. + * + * @copyright 2018 Zig Tan + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class subsystem_link implements type { + + /** + * @var The name of the core subsystem to link. + */ + protected $name; + + /** + * @var string A description of what this subsystem is used to store. + */ + protected $summary; + + /** + * Constructor for the subsystem_link. + * + * @param string $name The name of the subsystem to link. + * @param string $summary A description of what is stored within this subsystem. + */ + public function __construct($name, $summary = '') { + if (debugging('', DEBUG_DEVELOPER)) { + $teststring = clean_param($summary, PARAM_STRINGID); + if ($teststring !== $summary) { + debugging("Summary information for use of the '{$name}' subsystem " . + "has an invalid langstring identifier: '{$summary}'", + DEBUG_DEVELOPER); + } + } + + $this->name = $name; + $this->summary = $summary; + } + + /** + * Function to return the name of this subsystem_link type. + * + * @return string $name + */ + public function get_name() { + return $this->name; + } + + /** + * A subsystem link does not define any fields itself. + * + * @return array + */ + public function get_privacy_fields() : array { + return null; + } + + /** + * A summary of what this subsystem is used for. + * + * @return string $summary + */ + public function get_summary() { + return $this->summary; + } +} diff --git a/privacy/classes/local/metadata/types/type.php b/privacy/classes/local/metadata/types/type.php new file mode 100644 index 00000000000..48807febc5e --- /dev/null +++ b/privacy/classes/local/metadata/types/type.php @@ -0,0 +1,56 @@ +. + +/** + * The base type interface which encapsulates a set of data held by a component with Moodle. + * + * @package core_privacy + * @copyright 2018 Zig Tan + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_privacy\local\metadata\types; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The base type interface which all metadata types must implement. + * + * @copyright 2018 Zig Tan + * @package core_privacy + */ +interface type { + + /** + * Get the name describing this type. + * + * @return string + */ + public function get_name(); + + /** + * A list of the fields and their usage description. + * + * @return array + */ + public function get_privacy_fields(); + + /** + * A summary of what the metalink type is used for. + * + * @return string $summary + */ + public function get_summary(); +} diff --git a/privacy/classes/local/metadata/types/user_preference.php b/privacy/classes/local/metadata/types/user_preference.php new file mode 100644 index 00000000000..ebd587f1d73 --- /dev/null +++ b/privacy/classes/local/metadata/types/user_preference.php @@ -0,0 +1,93 @@ +. + +/** + * This file defines an item of metadata which encapsulates a user's preferences. + * + * @package core_privacy + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_privacy\local\metadata\types; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The user_preference type. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class user_preference implements type { + + /** + * @var The name of this user preference. + */ + protected $name; + + /** + * @var A description of what this user preference means. + */ + protected $summary; + + /** + * Constructor to create a new user_preference types. + * + * @param string $name The name of the user preference. + * @param string $summary A description of what the preference is used for. + */ + public function __construct($name, $summary = '') { + if (debugging('', DEBUG_DEVELOPER)) { + $teststring = clean_param($summary, PARAM_STRINGID); + if ($teststring !== $summary) { + debugging("Summary information for use of the '{$name}' subsystem " . + " has an invalid langstring identifier: '{$summary}'", + DEBUG_DEVELOPER); + } + } + + $this->name = $name; + $this->summary = $summary; + } + + /** + * The name of the user preference. + * + * @return string + */ + public function get_name() { + return $this->name; + } + + /** + * A user preference encapsulates a single field and has no sub-fields. + * + * @return array + */ + public function get_privacy_fields() { + return null; + } + + /** + * A summary of what this user preference is used for. + * + * @return string + */ + public function get_summary() { + return $this->summary; + } +} diff --git a/privacy/classes/local/request/approved_contextlist.php b/privacy/classes/local/request/approved_contextlist.php new file mode 100644 index 00000000000..7704332746a --- /dev/null +++ b/privacy/classes/local/request/approved_contextlist.php @@ -0,0 +1,75 @@ +. + +/** + * An implementation of a contextlist which has been filtered and approved. + * + * @package core_privacy + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_privacy\local\request; + +defined('MOODLE_INTERNAL') || die(); + +/** + * An implementation of a contextlist which has been filtered and approved. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class approved_contextlist extends contextlist_base { + + /** + * @var \stdClass The user this contextlist belongs to. + */ + protected $user; + + /** + * Create a new approved contextlist. + * + * @param \stdClass $user The user record. + * @param string $component the frankenstyle component name. + * @param \int[] $contextids The list of contextids present in this list. + */ + public function __construct(\stdClass $user, string $component, array $contextids) { + $this->set_user($user); + $this->set_component($component); + $this->set_contextids($contextids); + } + + /** + * Specify the user which owns this request. + * + * @param \stdClass $user The user record. + * @return $this + */ + protected function set_user(\stdClass $user) : approved_contextlist { + $this->user = $user; + + return $this; + } + + /** + * Get the user which requested their data. + * + * @return \stdClass + */ + public function get_user() : \stdClass { + return $this->user; + } +} diff --git a/privacy/classes/local/request/content_writer.php b/privacy/classes/local/request/content_writer.php new file mode 100644 index 00000000000..da60079b7a5 --- /dev/null +++ b/privacy/classes/local/request/content_writer.php @@ -0,0 +1,143 @@ +. + +/** + * This file contains the interface required to implmeent a content writer. + * + * @package core_privacy + * @copyright 2018 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_privacy\local\request; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The interface for a Moodle content writer. + * + * @package core_privacy + * @copyright 2018 Jake Dallimore + */ +interface content_writer { + + /** + * Constructor for the content writer. + * + * Note: The writer_factory must be passed. + * @param writer $writer The factory. + */ + public function __construct(writer $writer); + + /** + * Set the context for the current item being processed. + * + * @param \context $context The context to use + * @return content_writer + */ + public function set_context(\context $context) : content_writer ; + + /** + * Export the supplied data within the current context, at the supplied subcontext. + * + * @param array $subcontext The location within the current context that this data belongs. + * @param \stdClass $data The data to be exported + * @return content_writer + */ + public function export_data(array $subcontext, \stdClass $data) : content_writer ; + + /** + * Export metadata about the supplied subcontext. + * + * Metadata consists of a key/value pair and a description of the value. + * + * @param array $subcontext The location within the current context that this data belongs. + * @param string $name The metadata name. + * @param string $value The metadata value. + * @param string $description The description of the value. + * @return content_writer + */ + public function export_metadata(array $subcontext, string $name, $value, string $description) : content_writer ; + + /** + * Export a piece of related data. + * + * @param array $subcontext The location within the current context that this data belongs. + * @param string $name The name of the file to be exported. + * @param \stdClass $data The related data to export. + * @return content_writer + */ + public function export_related_data(array $subcontext, $name, $data) : content_writer ; + + /** + * Export a piece of data in a custom format. + * + * @param array $subcontext The location within the current context that this data belongs. + * @param string $filename The name of the file to be exported. + * @param string $filecontent The content to be exported. + * @return content_writer + */ + public function export_custom_file(array $subcontext, $filename, $filecontent) : content_writer ; + + /** + * Prepare a text area by processing pluginfile URLs within it. + * + * @param array $subcontext The location within the current context that this data belongs. + * @param string $component The name of the component that the files belong to. + * @param string $filearea The filearea within that component. + * @param string $itemid Which item those files belong to. + * @param string $text The text to be processed + * @return string The processed string + */ + public function rewrite_pluginfile_urls(array $subcontext, $component, $filearea, $itemid, $text) : string; + + /** + * Export all files within the specified component, filearea, itemid combination. + * + * @param array $subcontext The location within the current context that this data belongs. + * @param string $component The name of the component that the files belong to. + * @param string $filearea The filearea within that component. + * @param string $itemid Which item those files belong to. + * @return content_writer + */ + public function export_area_files(array $subcontext, $component, $filearea, $itemid) : content_writer ; + + /** + * Export the specified file in the target location. + * + * @param array $subcontext The location within the current context that this data belongs. + * @param \stored_file $file The file to be exported. + * @return content_writer + */ + public function export_file(array $subcontext, \stored_file $file) : content_writer ; + + /** + * Export the specified user preference. + * + * @param string $component The name of the component. + * @param string $key The name of th key to be exported. + * @param string $value The value of the preference + * @param string $description A description of the value + * @return content_writer + */ + public function export_user_preference(string $component, string $key, string $value, string $description) : content_writer ; + + /** + * Perform any required finalisation steps and return the location of the finalised export. + * + * @return string + */ + public function finalise_content() : string ; +} diff --git a/privacy/classes/local/request/contextlist.php b/privacy/classes/local/request/contextlist.php new file mode 100644 index 00000000000..47fc58bc347 --- /dev/null +++ b/privacy/classes/local/request/contextlist.php @@ -0,0 +1,72 @@ +. + +/** + * Privacy Fetch Result Set. + * + * @package core_privacy + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_privacy\local\request; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Fetch Result Set. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class contextlist extends contextlist_base { + + /** + * Add a set of contexts from SQL. + * + * The SQL should only return a list of context IDs. + * + * @param string $sql The SQL which will fetch the list of * context IDs + * @param array $params The set of SQL parameters + * @return $this + */ + public function add_from_sql(string $sql, array $params) : contextlist { + global $DB; + + $fields = \context_helper::get_preload_record_columns_sql('ctx'); + $wrapper = "SELECT {$fields} FROM {context} ctx WHERE id IN ({$sql})"; + $contexts = $DB->get_recordset_sql($wrapper, $params); + + $contextids = []; + foreach ($contexts as $context) { + $contextids[] = $context->ctxid; + \context_helper::preload_from_record($context); + } + + $this->set_contextids(array_merge($this->get_contextids(), $contextids)); + + return $this; + } + + /** + * Sets the component for this contextlist. + * + * @param string $component the frankenstyle component name. + */ + public function set_component($component) { + parent::set_component($component); + } +} diff --git a/privacy/classes/local/request/contextlist_base.php b/privacy/classes/local/request/contextlist_base.php new file mode 100644 index 00000000000..71e601a6eff --- /dev/null +++ b/privacy/classes/local/request/contextlist_base.php @@ -0,0 +1,161 @@ +. + +/** + * Base implementation of a contextlist. + * + * @package core_privacy + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_privacy\local\request; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Base implementation of a contextlist used to store a set of contexts. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class contextlist_base implements + // Implement an Iterator to fetch the Context objects. + \Iterator, + + // Implement the Countable interface to allow the number of returned results to be queried easily. + \Countable { + + /** + * @var array List of context IDs. + * + * Note: this must not be updated using set_contextids only as this + * ensures uniqueness. + */ + private $contextids = []; + + /** + * @var string component the frankenstyle component name. + */ + protected $component = ''; + + /** + * @var int Current position of the iterator. + */ + protected $iteratorposition = 0; + + /** + * Set the contextids. + * + * @param array $contextids The list of contexts. + */ + protected function set_contextids(array $contextids) { + $this->contextids = array_unique($contextids); + } + + /** + * Get the list of context IDs that relate to this request. + * + * @return int[] + */ + public function get_contextids() : array { + return $this->contextids; + } + + /** + * Get the complete list of context objects that relate to this + * request. + * + * @return \contect[] + */ + public function get_contexts() : array { + $contexts = []; + foreach ($this->contextids as $contextid) { + $contexts[] = \context::instance_by_id($contextid); + } + + return $contexts; + } + + /** + * Sets the component for this contextlist. + * + * @param string $component the frankenstyle component name. + */ + protected function set_component($component) { + $this->component = $component; + } + + /** + * Get the name of the component to which this contextlist belongs. + * + * @return string the component name associated with this contextlist. + */ + public function get_component() : string { + return $this->component; + } + + /** + * Return the current context. + * + * @return \context + */ + public function current() { + return \context::instance_by_id($this->contextids[$this->iteratorposition]); + } + + /** + * Return the key of the current element. + * + * @return mixed + */ + public function key() { + return $this->iteratorposition; + } + + /** + * Move to the next context in the list. + */ + public function next() { + ++$this->iteratorposition; + } + + /** + * Check if the current position is valid. + * + * @return bool + */ + public function valid() { + return isset($this->contextids[$this->iteratorposition]); + } + + /** + * Rewind to the first found context. + * + * The list of contexts is uniqued during the rewind. + * The rewind is called at the start of most iterations. + */ + public function rewind() { + $this->iteratorposition = 0; + } + + /** + * Return the number of contexts. + */ + public function count() { + return count($this->contextids); + } +} diff --git a/privacy/classes/local/request/contextlist_collection.php b/privacy/classes/local/request/contextlist_collection.php new file mode 100644 index 00000000000..722b541404b --- /dev/null +++ b/privacy/classes/local/request/contextlist_collection.php @@ -0,0 +1,180 @@ +. + +/** + * This file defines the contextlist_collection class object. + * + * The contextlist_collection is used to organize a collection of contextlists. + * + * @package core_privacy + * @copyright 2018 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_privacy\local\request; + +defined('MOODLE_INTERNAL') || die(); + +/** + * A collection of contextlist items. + * + * @copyright 2018 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class contextlist_collection implements \Iterator, \Countable { + + /** + * @var int $userid The ID of the user that the contextlist collection belongs to. + */ + protected $userid = null; + + /** + * @var array $contextlists the internal array of contextlist objects. + */ + protected $contextlists = []; + + /** + * @var int Current position of the iterator. + */ + protected $iteratorposition = 0; + + /** + * Constructor to create a new contextlist_collection. + * + * @param int $userid The userid to which this collection belongs. + */ + public function __construct($userid) { + $this->userid = $userid; + } + + /** + * Return the ID of the user whose collection this is. + * + * @return int + */ + public function get_userid() : int { + return $this->userid; + } + + /** + * Add a contextlist to this collection. + * + * @param contextlist_base $contextlist the contextlist to export. + * @return $this + */ + public function add_contextlist(contextlist_base $contextlist) { + $component = $contextlist->get_component(); + if (empty($component)) { + throw new \moodle_exception("The contextlist must have a component set"); + } + if (isset($this->contextlists[$component])) { + throw new \moodle_exception("A contextlist has already been added for the '{$component}' component"); + } + + $this->contextlists[$component] = $contextlist; + + return $this; + } + + /** + * Get the contextlists in this collection. + * + * @return array the associative array of contextlists in this collection, indexed by component name. + * E.g. mod_assign => contextlist, core_comment => contextlist. + */ + public function get_contextlists() : array { + return $this->contextlists; + } + + /** + * Get the contextlist for the specified component. + * + * @param string $component the frankenstyle name of the component to fetch for. + * @return contextlist_base|null + */ + public function get_contextlist_for_component(string $component) { + if (isset($this->contextlists[$component])) { + return $this->contextlists[$component]; + } + + return null; + } + + /** + * Return the current contexlist. + * + * @return \context + */ + public function current() { + $key = $this->get_key_from_position(); + return $this->contextlists[$key]; + } + + /** + * Return the key of the current element. + * + * @return mixed + */ + public function key() { + return $this->get_key_from_position(); + } + + /** + * Move to the next context in the list. + */ + public function next() { + ++$this->iteratorposition; + } + + /** + * Check if the current position is valid. + * + * @return bool + */ + public function valid() { + return ($this->iteratorposition < count($this->contextlists)); + } + + /** + * Rewind to the first found context. + * + * The list of contexts is uniqued during the rewind. + * The rewind is called at the start of most iterations. + */ + public function rewind() { + $this->iteratorposition = 0; + } + + /** + * Get the key for the current iterator position. + * + * @return string + */ + protected function get_key_from_position() { + $keylist = array_keys($this->contextlists); + if (isset($keylist[$this->iteratorposition])) { + return $keylist[$this->iteratorposition]; + } + + return null; + } + + /** + * Return the number of contexts. + */ + public function count() { + return count($this->contextlists); + } +} diff --git a/privacy/classes/local/request/core_data_provider.php b/privacy/classes/local/request/core_data_provider.php new file mode 100644 index 00000000000..54f8bcf6db1 --- /dev/null +++ b/privacy/classes/local/request/core_data_provider.php @@ -0,0 +1,44 @@ +. + +/** + * This file contains the \core_privacy\local\request\core_data_provider interface to describe + * classes which provide data in some form to core. + * + * @package core_privacy + * @copyright 2018 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_privacy\local\request; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The core_data_provider interface is used to describe a provider which + * services user requests between components and core. + * + * It does not define a specific way of doing so and different types of + * data will need to extend this interface in order to define their own + * contract. + * + * It should not be implemented directly, but should be extended by other + * interfaces in core. + * + * @package core_privacy + * @copyright 2018 Jake Dallimore + */ +interface core_data_provider extends data_provider { +} diff --git a/privacy/classes/local/request/core_user_data_provider.php b/privacy/classes/local/request/core_user_data_provider.php new file mode 100644 index 00000000000..979aa7ab274 --- /dev/null +++ b/privacy/classes/local/request/core_user_data_provider.php @@ -0,0 +1,69 @@ +. + +/** + * This file contains the \core_privacy\local\request\core_user_data_provider interface to describe + * classes which provide user data in some form to core. + * + * @package core_privacy + * @copyright 2018 Jake Dallimore + * + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_privacy\local\request; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The core_user_data_provider interface is used to describe a provider + * which services user requests between components and core. + * + * It describes data how these requests are serviced in a specific format. + * + * @package core_privacy + * @copyright 2018 Jake Dallimore + */ +interface core_user_data_provider extends core_data_provider { + + /** + * Get the list of contexts that contain user information for the specified user. + * + * @param int $userid The user to search. + * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin. + */ + public static function get_contexts_for_userid(int $userid) : contextlist; + + /** + * Export all user data for the specified user, in the specified contexts. + * + * @param approved_contextlist $contextlist The approved contexts to export information for. + */ + public static function export_user_data(approved_contextlist $contextlist); + + /** + * Delete all use data which matches the specified deletion_criteria. + * + * @param deletion_criteria $criteria An object containing specific deletion criteria to delete for. + */ + public static function delete_for_context(deletion_criteria $criteria); + + /** + * Delete all user data for the specified user, in the specified contexts. + * + * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. + */ + public static function delete_user_data(approved_contextlist $contextlist); +} diff --git a/privacy/classes/local/request/data_provider.php b/privacy/classes/local/request/data_provider.php new file mode 100644 index 00000000000..5bc27b8ad0f --- /dev/null +++ b/privacy/classes/local/request/data_provider.php @@ -0,0 +1,49 @@ +. + +/** + * This file contains the \core_privacy\local\request\data_provider interface to describe + * a class which provides data in some form. + * + * @package core_privacy + * @copyright 2018 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_privacy\local\request; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The data_provider interface is used to describe a provider + * which services user requests in any fashion. This includes both + * -- component <-> core; and + * -- component <-> component. + * + * It does not define a specific way of doing so and different types of + * data will need to extend this interface in order to define their own + * contract. + * + * It should not be implemented directly, but should be extended by other + * interfaces in core. + * + * This is the base interface for any component which stores any form of + * user data. + * + * @package core_privacy + * @copyright 2018 Jake Dallimore + */ +interface data_provider { +} diff --git a/privacy/classes/local/request/deletion_criteria.php b/privacy/classes/local/request/deletion_criteria.php new file mode 100644 index 00000000000..2a4083a8691 --- /dev/null +++ b/privacy/classes/local/request/deletion_criteria.php @@ -0,0 +1,58 @@ +. + +/** + * The \core_privacy\local\request\deletion_criteria class. + * + * @package core_privacy + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_privacy\local\request; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The deletion_criteria class is used to describe conditions for a set of + * data due to be deleted. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class deletion_criteria { + /** + * @var context The context being deleted. + */ + protected $context = null; + + /** + * Constructor for a new deletion_criteria. + * + * @param \context $context The context being deleted. + */ + public function __construct(\context $context) { + $this->context = $context; + } + + /** + * Get the context to be deleted. + * + * @return \context + */ + public function get_context() : \context { + return $this->context; + } +} diff --git a/privacy/classes/local/request/helper.php b/privacy/classes/local/request/helper.php new file mode 100644 index 00000000000..a539307993b --- /dev/null +++ b/privacy/classes/local/request/helper.php @@ -0,0 +1,301 @@ +. + +/** + * This file contains the core_privacy\local\request helper. + * + * @package core_privacy + * @copyright 2018 Andrew Nicols + * + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_privacy\local\request; + +use \core_privacy\local\request\writer; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/modinfolib.php'); +require_once($CFG->dirroot . '/course/modlib.php'); + +/** + * The core_privacy\local\request\helper class with useful shared functionality. + * + * @package core_privacy + * @copyright 2018 Andrew Nicols + * + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class helper { + + /** + * Add core-controlled contexts which are related to a component but that component may know about. + * + * For example, most activities are not aware of activity completion, but the course implements it for them. + * These should be included. + * + * @param int $userid The user being added for. + * @param contextlist $contextlist The contextlist being appended to. + * @return contextlist The final contextlist + */ + public static function add_shared_contexts_to_contextlist_for(int $userid, contextlist $contextlist) : contextlist { + if (strpos($contextlist->get_component(), 'mod_') === 0) { + // Activity modules support data stored by core about them - for example, activity completion. + $contextlist = static::add_shared_contexts_to_contextlist_for_course_module($userid, $contextlist); + } + + return $contextlist; + } + + /** + * Handle export of standard data for a plugin which implements the null provider and does not normally store data + * of its own. + * + * This is used in cases such as activities like mod_resource, which do not store their own data, but may still have + * data on them (like Activity Completion). + * + * Any context provided in a contextlist should have base data exported as a minimum. + * + * @param approved_contextlist $contextlist The approved contexts to export information for. + */ + public static function export_data_for_null_provider(approved_contextlist $contextlist) { + $user = $contextlist->get_user(); + foreach ($contextlist as $context) { + $data = static::get_context_data($context, $user); + static::export_context_files($context, $user); + + writer::with_context($context)->export_data([], $data); + } + } + + /** + * Handle removal of 'standard' data for any plugin. + * + * This will handle deletion for things such as activity completion. + * + * @param string $component The component being deleted for. + * @param deletion_criteria $criteria An object containing specific deletion criteria to delete for. + */ + public static function delete_for_context(string $component, deletion_criteria $criteria) { + if (strpos($component, 'mod_') === 0) { + // Activity modules support data stored by core about them - for example, activity completion. + static::delete_for_context_course_module($component, $criteria->get_context()); + } + } + + /** + * Delete all 'standard' user data for the specified user, in the specified contexts. + * + * This will handle deletion for things such as activity completion. + * + * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. + */ + public static function delete_user_data(approved_contextlist $contextlist) { + $component = $contextlist->get_component(); + + if (strpos($component, 'mod_') === 0) { + // Activity modules support data stored by core about them - for example, activity completion. + static::delete_user_data_for_course_module($contextlist); + } + } + + /** + * Get all general data for this context. + * + * @param \context $context The context to retrieve data for. + * @param \stdClass $user The user being written. + * @return \stdClass + */ + public static function get_context_data(\context $context, \stdClass $user) : \stdClass { + global $DB; + + $basedata = (object) []; + if ($context instanceof \context_module) { + return static::get_context_module_data($context, $user); + } + if ($context instanceof \context_block) { + return static::get_context_block_data($context, $user); + } + + return $basedata; + } + + /** + * Export all files for this context. + * + * @param \context $context The context to export files for. + * @param \stdClass $user The user being written. + * @return \stdClass + */ + public static function export_context_files(\context $context, \stdClass $user) { + if ($context instanceof \context_module) { + return static::export_context_module_files($context, $user); + } + } + + /** + * Add core-controlled contexts which are related to a component but that component may know about. + * + * For example, most activities are not aware of activity completion, but the course implements it for them. + * These should be included. + * + * @param int $userid The user being added for. + * @param contextlist $contextlist The contextlist being appended to. + * @return contextlist The final contextlist + */ + protected static function add_shared_contexts_to_contextlist_for_course_module(int $userid, contextlist $contextlist) : contextlist { + // Fetch all contexts where the user has activity completion enabled. + $sql = "SELECT + c.id + FROM {course_modules_completion} cmp + INNER JOIN {course_modules} cm ON cm.id = cmp.coursemoduleid + INNER JOIN {modules} m ON m.id = cm.module + INNER JOIN {context} c ON c.instanceid = cm.id AND c.contextlevel = :contextlevel + WHERE cmp.userid = :userid + AND m.name = :modname"; + $params = [ + 'userid' => $userid, + // Strip the mod_ from the name. + 'modname' => substr($contextlist->get_component(), 4), + 'contextlevel' => CONTEXT_MODULE, + ]; + + $contextlist->add_from_sql($sql, $params); + + return $contextlist; + } + + /** + * Get all general data for the activity module at this context. + * + * @param \context_module $context The context to retrieve data for. + * @param \stdClass $user The user being written. + * @return \stdClass + */ + protected static function get_context_module_data(\context_module $context, \stdClass $user) : \stdClass { + global $DB; + + $coursecontext = $context->get_course_context(); + $modinfo = get_fast_modinfo($coursecontext->instanceid); + $cm = $modinfo->cms[$context->instanceid]; + $component = "mod_{$cm->modname}"; + $course = $cm->get_course(); + $moduledata = $DB->get_record($cm->modname, ['id' => $cm->instance]); + + $basedata = (object) [ + 'name' => $cm->get_formatted_name(), + ]; + + if (plugin_supports('mod', $cm->modname, FEATURE_MOD_INTRO, true)) { + $intro = $moduledata->intro; + + $intro = writer::with_context($context) + ->rewrite_pluginfile_urls([], $component, 'intro', 0, $intro); + + $options = [ + 'noclean' => true, + 'para' => false, + 'context' => $context, + 'overflowdiv' => true, + ]; + $basedata->intro = format_text($intro, $moduledata->introformat, $options); + } + + // Completion tracking. + $completioninfo = new \completion_info($course); + $completion = $completioninfo->is_enabled($cm); + if ($completion != COMPLETION_TRACKING_NONE) { + $completiondata = $completioninfo->get_data($cm, true, $user->id); + $basedata->completion = (object) [ + 'state' => $completiondata->completionstate, + ]; + } + + return $basedata; + } + + /** + * Get all general data for the block at this context. + * + * @param \context_block $context The context to retrieve data for. + * @param \stdClass $user The user being written. + * @return \stdClass General data about this block instance. + */ + protected static function get_context_block_data(\context_block $context, \stdClass $user) : \stdClass { + global $DB; + + $block = $DB->get_record('block_instances', ['id' => $context->instanceid]); + + $basedata = (object) [ + 'blocktype' => get_string('pluginname', 'block_' . $block->blockname) + ]; + + return $basedata; + } + + /** + * Get all general data for the activity module at this context. + * + * @param \context_module $context The context to retrieve data for. + * @param \stdClass $user The user being written. + * @return \stdClass + */ + protected static function export_context_module_files(\context_module $context, \stdClass $user) { + $coursecontext = $context->get_course_context(); + $modinfo = get_fast_modinfo($coursecontext->instanceid); + $cm = $modinfo->cms[$context->instanceid]; + $component = "mod_{$cm->modname}"; + + writer::with_context($context) + // Export the files for the intro. + ->export_area_files([], $component, 'intro', 0); + } + + /** + * Handle removal of 'standard' data for course modules. + * + * This will handle deletion for things such as activity completion. + * + * @param string $component The component being deleted for. + * @param \context_module $context The context to delete all data for. + */ + public static function delete_for_context_course_module(string $component, \context_module $context) { + global $DB; + + // Delete course completion data for this context. + $DB->delete_records('course_modules_completion', ['coursemoduleid' => $context->instanceid]); + } + + /** + * Delete all 'standard' user data for the specified user in course modules. + * + * This will handle deletion for things such as activity completion. + * + * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. + */ + protected static function delete_user_data_for_course_module(approved_contextlist $contextlist) { + global $DB; + + foreach ($contextlist as $context) { + // Delete course completion data for this context. + $DB->delete_records('course_modules_completion', [ + 'coursemoduleid' => $context->instanceid, + 'userid' => $contextlist->get_user()->id, + ]); + } + + } +} diff --git a/privacy/classes/local/request/moodle_content_writer.php b/privacy/classes/local/request/moodle_content_writer.php new file mode 100644 index 00000000000..82c997348d1 --- /dev/null +++ b/privacy/classes/local/request/moodle_content_writer.php @@ -0,0 +1,320 @@ +. + +/** + * This file contains the moodle format implementation of the content writer. + * + * @package core_privacy + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_privacy\local\request; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The moodle_content_writer is the default Moodle implementation of a content writer. + * + * It exports data to a rich tree structure using Moodle's context system, + * and produces a single zip file with all content. + * + * Objects of data are stored as JSON. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class moodle_content_writer implements content_writer { + /** + * @var string The base path on disk for this instance. + */ + protected $path = null; + + /** + * @var \context The current context of the writer. + */ + protected $context = null; + + /** + * @var \stored_file[] The list of files to be exported. + */ + protected $files = []; + + /** + * Constructor for the content writer. + * + * Note: The writer factory must be passed. + * + * @param writer $writer The factory. + */ + public function __construct(writer $writer) { + $this->path = make_request_directory(); + } + + /** + * Set the context for the current item being processed. + * + * @param \context $context The context to use + */ + public function set_context(\context $context) : content_writer { + $this->context = $context; + + return $this; + } + + /** + * Export the supplied data within the current context, at the supplied subcontext. + * + * @param array $subcontext The location within the current context that this data belongs. + * @param \stdClass $data The data to be exported + */ + public function export_data(array $subcontext, \stdClass $data) : content_writer { + $path = $this->get_path($subcontext, 'data.json'); + + $this->write_data($path, json_encode($data)); + + return $this; + } + + /** + * Export metadata about the supplied subcontext. + * + * Metadata consists of a key/value pair and a description of the value. + * + * @param array $subcontext The location within the current context that this data belongs. + * @param string $key The metadata name. + * @param string $value The metadata value. + * @param string $description The description of the value. + */ + public function export_metadata(array $subcontext, string $key, $value, string $description) : content_writer { + $path = $this->get_full_path($subcontext, 'metadata.json'); + + if (file_exists($path)) { + $data = json_decode(file_get_contents($path)); + } else { + $data = (object) []; + } + + $data->$key = (object) [ + 'value' => $value, + 'description' => $description, + ]; + + $path = $this->get_path($subcontext, 'metadata.json'); + $this->write_data($path, json_encode($data)); + + return $this; + } + + /** + * Export a piece of related data. + * + * @param array $subcontext The location within the current context that this data belongs. + * @param string $name The name of the file to be exported. + * @param \stdClass $data The related data to export. + */ + public function export_related_data(array $subcontext, $name, $data) : content_writer { + $path = $this->get_path($subcontext, "{$name}.json"); + + $this->write_data($path, json_encode($data)); + + return $this; + } + + /** + * Export a piece of data in a custom format. + * + * @param array $subcontext The location within the current context that this data belongs. + * @param string $filename The name of the file to be exported. + * @param string $filecontent The content to be exported. + */ + public function export_custom_file(array $subcontext, $filename, $filecontent) : content_writer { + $filename = clean_param($filename, PARAM_FILE); + $path = $this->get_path($subcontext, $filename); + $this->write_data($path, $filecontent); + + return $this; + } + + /** + * Prepare a text area by processing pluginfile URLs within it. + * + * @param array $subcontext The location within the current context that this data belongs. + * @param string $component The name of the component that the files belong to. + * @param string $filearea The filearea within that component. + * @param string $itemid Which item those files belong to. + * @param string $text The text to be processed + * @return string The processed string + */ + public function rewrite_pluginfile_urls(array $subcontext, $component, $filearea, $itemid, $text) : string { + return str_replace('@@PLUGINFILE@@/', 'files/', $text); + } + + /** + * Export all files within the specified component, filearea, itemid combination. + * + * @param array $subcontext The location within the current context that this data belongs. + * @param string $component The name of the component that the files belong to. + * @param string $filearea The filearea within that component. + * @param string $itemid Which item those files belong to. + */ + public function export_area_files(array $subcontext, $component, $filearea, $itemid) : content_writer { + $fs = get_file_storage(); + $files = $fs->get_area_files($this->context->id, $component, $filearea, $itemid); + foreach ($files as $file) { + $this->export_file($subcontext, $file); + } + + return $this; + } + + /** + * Export the specified file in the target location. + * + * @param array $subcontext The location within the current context that this data belongs. + * @param \stored_file $file The file to be exported. + */ + public function export_file(array $subcontext, \stored_file $file) : content_writer { + if (!$file->is_directory()) { + $subcontextextra = [ + get_string('files'), + $file->get_filepath(), + ]; + $path = $this->get_path(array_merge($subcontext, $subcontextextra), $file->get_filename()); + check_dir_exists(dirname($path), true, true); + $this->files[$path] = $file; + } + + return $this; + } + + /** + * Export the specified user preference. + * + * @param string $component The name of the component. + * @param string $key The name of th key to be exported. + * @param string $value The value of the preference + * @param string $description A description of the value + * @return content_writer + */ + public function export_user_preference(string $component, string $key, string $value, string $description) : content_writer { + if ($this->context !== \context_system::instance()) { + throw new \coding_exception('export_user_preference must be called against the system context'); + } + $subcontext = [ + get_string('userpreferences'), + ]; + $fullpath = $this->get_full_path($subcontext, "{$component}.json"); + $path = $this->get_path($subcontext, "{$component}.json"); + + if (file_exists($fullpath)) { + $data = json_decode(file_get_contents($fullpath)); + } else { + $data = (object) []; + } + + $data->$key = (object) [ + 'value' => $value, + 'description' => $description, + ]; + $this->write_data($path, json_encode($data)); + + return $this; + } + + /** + * Determine the path for the current context. + * + * @return array The context path. + */ + protected function get_context_path() : Array { + $path = []; + $contexts = array_reverse($this->context->get_parent_contexts(true)); + foreach ($contexts as $context) { + $path[] = clean_param($context->get_context_name(), PARAM_FILE); + } + + return $path; + } + + /** + * Get the relative file path within the current context, and subcontext, using the specified filename. + * + * @param string[] $subcontext The location within the current context to export this data. + * @param string $name The intended filename, including any extensions. + * @return string The fully-qualfiied file path. + */ + protected function get_path(array $subcontext, string $name) : string { + // Combine the context path, and the subcontext data. + $path = array_merge( + $this->get_context_path(), + $subcontext + ); + + // Join the directory together with the name. + $filepath = implode(DIRECTORY_SEPARATOR, $path) . DIRECTORY_SEPARATOR . $name; + + return preg_replace('@' . DIRECTORY_SEPARATOR . '+@', DIRECTORY_SEPARATOR, $filepath); + } + + /** + * Get the fully-qualified file path within the current context, and subcontext, using the specified filename. + * + * @param string[] $subcontext The location within the current context to export this data. + * @param string $name The intended filename, including any extensions. + * @return string The fully-qualfiied file path. + */ + protected function get_full_path(array $subcontext, string $name) : string { + $path = array_merge( + [$this->path], + [$this->get_path($subcontext, $name)] + ); + + // Join the directory together with the name. + $filepath = implode(DIRECTORY_SEPARATOR, $path); + + return preg_replace('@' . DIRECTORY_SEPARATOR . '+@', DIRECTORY_SEPARATOR, $filepath); + } + + /** + * Write the data to the specified path. + * + * @param string $path The path to export the data at. + * @param string $data The data to be exported. + */ + protected function write_data(string $path, string $data) { + $targetpath = $this->path . DIRECTORY_SEPARATOR . $path; + check_dir_exists(dirname($targetpath), true, true); + file_put_contents($targetpath, $data); + $this->files[$path] = $targetpath; + } + + /** + * Perform any required finalisation steps and return the location of the finalised export. + * + * @return string + */ + public function finalise_content() : string { + $exportfile = make_request_directory() . '/export.zip'; + + $fp = get_file_packer(); + $fp->archive_to_pathname($this->files, $exportfile); + + // Reset the writer to prevent any further writes. + writer::reset(); + + return $exportfile; + } +} diff --git a/privacy/classes/local/request/plugin/provider.php b/privacy/classes/local/request/plugin/provider.php new file mode 100644 index 00000000000..c748af704cb --- /dev/null +++ b/privacy/classes/local/request/plugin/provider.php @@ -0,0 +1,39 @@ +. + +/** + * This file contains the \core_privacy\local\request\plugin\provider interface to describe + * a class which provides data in some form for a plugin. + * + * Plugins should implement this if they store any personal information. + * + * @package core_privacy + * @copyright 2018 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_privacy\local\request\plugin; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The provider interface for plugins which provide data from a plugin + * directly to the Privacy subsystem. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +interface provider extends \core_privacy\local\request\core_user_data_provider { +} diff --git a/privacy/classes/local/request/plugin/subplugin_provider.php b/privacy/classes/local/request/plugin/subplugin_provider.php new file mode 100644 index 00000000000..6f6cc54176f --- /dev/null +++ b/privacy/classes/local/request/plugin/subplugin_provider.php @@ -0,0 +1,42 @@ +. + +/** + * This file contains the \core_privacy\local\request\plugin\subplugin_provider + * interface to describe a class which provides data in some form for the + * subplugin of another plugin. + * + * It should not be implemented directly, but should be extended by the + * plugin providing a subplugin. + * + * @package core_privacy + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_privacy\local\request\plugin; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The subplugin_provider interface is for plugins which are sub-plugins of + * a plugin. They do not provide data directly to the core Privacy + * subsystem, but will be accessed and called via the plugin itself. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +interface subplugin_provider extends \core_privacy\local\request\shared_data_provider { +} diff --git a/privacy/classes/local/request/plugin/subsystem_provider.php b/privacy/classes/local/request/plugin/subsystem_provider.php new file mode 100644 index 00000000000..4b036ae5bf2 --- /dev/null +++ b/privacy/classes/local/request/plugin/subsystem_provider.php @@ -0,0 +1,54 @@ +. + +/** + * This file contains the \core_privacy\local\request\plugin\subsystem_provider + * interface to describe a class which provides data in some form for a + * subsystem. + * + * It should not be implemented directly, but should be extended by the + * subsystem responsible for the plugintype. + * + * @package core_privacy + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_privacy\local\request\plugin; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The subsystem_provider interface is for plugins which may not + * necessarily be called directly, but instead via a subsystem. + * + * One example of this is the questiontype plugintype. These are + * intrinsically linked against the question subsystem and the question + * subsystem should define an interface extending this one through which it + * can query and retrieve specific data from each questiontype as required. + * + * Each questiontype may additionally respond directly to the privacy API + * if it also impleents the \core_privacay\local\request\plugin\provider + * interface directly. + * + * Care should be taken when extending this provider to not conflict with + * the \core_privacay\local\request\plugin\provider interface. + * + * @package core_privacy + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +interface subsystem_provider extends \core_privacy\local\request\shared_data_provider { +} diff --git a/privacy/classes/local/request/shared_data_provider.php b/privacy/classes/local/request/shared_data_provider.php new file mode 100644 index 00000000000..12ef7ef5437 --- /dev/null +++ b/privacy/classes/local/request/shared_data_provider.php @@ -0,0 +1,48 @@ +. + +/** + * This file contains the \core_privacy\local\request\shared_data_provider interface to describe + * a class which provides data in some form. + * + * @package core_privacy + * @copyright 2018 Jake Dallimore + * + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_privacy\local\request; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The shared_data_provider interface is used to describe a provider which + * services user requests between components and and other components. + * + * This includes communication between subplugin, subsystems, and plugins + * which are designed to interact closely with subsystems. + * + * It does not define a specific way of doing so and different types of + * data will need to extend this interface in order to define their own + * contract. + * + * It should not be implemented directly, but should be extended by other + * interfaces in core. + * + * @package core_privacy + * @copyright 2018 Jake Dallimore + */ +interface shared_data_provider extends data_provider { +} diff --git a/privacy/classes/local/request/subsystem/plugin_provider.php b/privacy/classes/local/request/subsystem/plugin_provider.php new file mode 100644 index 00000000000..2e41447e970 --- /dev/null +++ b/privacy/classes/local/request/subsystem/plugin_provider.php @@ -0,0 +1,36 @@ +. + +/** + * This file contains the \core_privacy\local\request\subsystem\plugin_provider interface to describe + * a class which provides data in some form for a subsystem. + * + * @package core_privacy + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_privacy\local\request\subsystem; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The plugin_provider interface for subsystems which provide data directly to a plugin. + * + * @package core_privacy + * @copyright 2018 Andrew Nicols + */ +interface plugin_provider extends \core_privacy\local\request\shared_data_provider { +} diff --git a/privacy/classes/local/request/subsystem/provider.php b/privacy/classes/local/request/subsystem/provider.php new file mode 100644 index 00000000000..ecd01c27138 --- /dev/null +++ b/privacy/classes/local/request/subsystem/provider.php @@ -0,0 +1,39 @@ +. + +/** + * This file contains the \core_privacy\local\request\subsystem\provider interface to describe + * a class which provides data in some form for a subsystem. + * + * Plugins should implement this if they directly store any personal information. + * + * @package core_privacy + * @copyright 2018 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_privacy\local\request\subsystem; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The provider interface for plugins which provide data from a subsystem + * directly to the Privacy subsystem. + * + * @package core_privacy + * @copyright 2018 Andrew Nicols + */ +interface provider extends \core_privacy\local\request\core_user_data_provider { +} diff --git a/privacy/classes/local/request/transform.php b/privacy/classes/local/request/transform.php new file mode 100644 index 00000000000..34a3bd15ad9 --- /dev/null +++ b/privacy/classes/local/request/transform.php @@ -0,0 +1,83 @@ +. + +/** + * This file contains the core_privacy\local\request helper. + * + * @package core_privacy + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_privacy\local\request; + +defined('MOODLE_INTERNAL') || die(); + +/** + * A class containing a set of data transformations for core data types. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class transform { + /** + * Translate a userid into the standard user format for exports. + * + * We have not determined if we will do this or not, but we provide the functionality and encourgae people to use + * it so that it can be retrospectively fitted if required. + * + * @param int $userid the userid to translate + * @return mixed + */ + public static function user(int $userid) { + // For the moment we do not think we should transform as this reveals information about other users. + // However this function is implemented should the need arise in the future. + return $userid; + } + + /** + * Translate a unix timestamp into a datetime string. + * + * @param int $datetime the unixtimestamp to translate. + * @return string The translated string. + */ + public static function datetime($datetime) { + return userdate($datetime, get_string('strftimedaydatetime', 'langconfig')); + } + + /** + * Translate a unix timestamp into a date string. + * + * @param int $date the unixtimestamp to translate. + * @return string The translated string. + */ + public static function date($date) { + return userdate($date, get_string('strftimetime', 'langconfig')); + } + + /** + * Translate a bool or int (0/1) value into a translated yes/no string. + * + * @param bool $value The value to translate + * @return string + */ + public static function yesno($value) { + if ($value) { + return get_string('yes'); + } else { + return get_string('no'); + } + } +} diff --git a/privacy/classes/local/request/user_preference_provider.php b/privacy/classes/local/request/user_preference_provider.php new file mode 100644 index 00000000000..92281f5d4d9 --- /dev/null +++ b/privacy/classes/local/request/user_preference_provider.php @@ -0,0 +1,46 @@ +. + +/** + * This file contains the \core_privacy\local\request\user_preference_provider interface to describe + * a class which provides preference data in some form to core. + * + * @package core_privacy + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_privacy\local\request; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The user_preference_provider interface is an interface designed to be + * implemented by components directly to describe a case where that + * component is responsible for storing some form of system-wide user + * preference. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +interface user_preference_provider extends core_data_provider { + + /** + * 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); +} diff --git a/privacy/classes/local/request/writer.php b/privacy/classes/local/request/writer.php new file mode 100644 index 00000000000..79a6c7ca676 --- /dev/null +++ b/privacy/classes/local/request/writer.php @@ -0,0 +1,118 @@ +. + +/** + * This file contains the interface required to implmeent a content writer. + * + * @package core_privacy + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_privacy\local\request; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The writer factory class used to fetch and work with the content_writer. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class writer { + /** + * @var writer The singleton instance of this writer. + */ + protected static $instance = null; + + /** + * @var content_writer The current content_writer instance. + */ + protected $realwriter = null; + + /** + * Constructor for the content writer. + * + * Protected to prevent direct instantiation. + */ + protected function __construct() { + } + + /** + * Singleton to return or create and return a copy of a content_writer. + * + * @return content_writer + */ + protected function get_writer_instance() : content_writer { + if (null === $this->realwriter) { + if (PHPUNIT_TEST) { + $this->realwriter = new \core_privacy\tests\request\content_writer(static::instance()); + } else { + $this->realwriter = new moodle_content_writer(static::instance()); + } + } + + return $this->realwriter; + } + + /** + * Return an instance of + */ + protected static final function instance() { + if (null === self::$instance) { + self::$instance = new static(); + } + + return self::$instance; + } + + /** + * Reset the writer and content_writer. + */ + public static final function reset() { + static::$instance = null; + } + + /** + * Provide an instance of the writer with the specified context applied. + * + * @param \context $context The context to apply + * @return content_writer The content_writer + */ + public static function with_context(\context $context) : content_writer { + return static::instance() + ->get_writer_instance() + ->set_context($context); + } + + /** + * Export the specified user preference. + * + * @param string $component The name of the component. + * @param string $key The name of th key to be exported. + * @param string $value The value of the preference + * @param string $description A description of the value + * @return content_writer + */ + public static function export_user_preference( + string $component, + string $key, + string $value, + string $description + ) : content_writer { + return static::with_context(\context_system::instance()) + ->export_user_preference($component, $key, $value, $description); + } +} diff --git a/privacy/classes/tests/provider_testcase.php b/privacy/classes/tests/provider_testcase.php new file mode 100644 index 00000000000..92b30b51621 --- /dev/null +++ b/privacy/classes/tests/provider_testcase.php @@ -0,0 +1,120 @@ +. + +/** + * Testcase for providers implementing parts of the core_privacy subsystem. + * + * @package core_privacy + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_privacy\tests; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +/** + * Testcase for providers implementing parts of the core_privacy subsystem. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class provider_testcase extends \advanced_testcase { + + /** + * Test tearDown. + */ + public function tearDown() { + \core_privacy\local\request\writer::reset(); + } + + /** + * Export all data for a component for the specified user. + * + * @param int $userid The userid of the user to fetch. + * @param string $component The component to get context data for. + * @return \core_privacy\local\request\contextlist + */ + public function get_contexts_for_userid(int $userid, string $component) { + $classname = $this->get_provider_classname($component); + + return $classname::get_contexts_for_userid($userid); + } + + /** + * Export all data for a component for the specified user. + * + * @param int $userid The userid of the user to fetch. + * @param string $component The component to get export data for. + */ + public function export_all_data_for_user(int $userid, string $component) { + $contextlist = $this->get_contexts_for_userid($userid, $component); + + $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist( + \core_user::get_user($userid), + $component, + $contextlist->get_contextids() + ); + + $classname = $this->get_provider_classname($component); + $classname::export_user_data($approvedcontextlist); + } + + /** + * Export all daa within a context for a component for the specified user. + * + * @param int $userid The userid of the user to fetch. + * @param \context $context The context to export data for. + * @param string $component The component to get export data for. + */ + public function export_context_data_for_user(int $userid, \context $context, string $component) { + $contextlist = new \core_privacy\tests\request\approved_contextlist( + \core_user::get_user($userid), + $component, + [$context->id] + ); + + $classname = $this->get_provider_classname($component); + $classname::export_user_data($contextlist); + } + + /** + * Determine the classname and ensure that it is a provider. + * + * @param string $component The classname. + * @return string + */ + protected function get_provider_classname($component) { + $classname = "\\${component}\\privacy\\provider"; + + if (!class_exists($classname)) { + throw new \coding_exception("{$component} does not implement any provider"); + } + + $rc = new \ReflectionClass($classname); + if (!$rc->implementsInterface(\core_privacy\local\metadata\provider::class)) { + throw new \coding_exception("{$component} does not implement metadata provider"); + } + + if (!$rc->implementsInterface(\core_privacy\local\request\core_user_data_provider::class)) { + throw new \coding_exception("{$component} does not declare that it provides any user data"); + } + + return $classname; + } +} diff --git a/privacy/classes/tests/request/approved_contextlist.php b/privacy/classes/tests/request/approved_contextlist.php new file mode 100644 index 00000000000..8ce7b01a1d3 --- /dev/null +++ b/privacy/classes/tests/request/approved_contextlist.php @@ -0,0 +1,79 @@ +. + +/** + * Approved result set for unit testing. + * + * @package core_privacy + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_privacy\tests\request; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Fetch Result Set. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class approved_contextlist extends \core_privacy\local\request\approved_contextlist { + /** + * Add a single context to this approved_contextlist. + * + * @param \context $context The context to be added. + * @return $this + */ + public function add_context(\context $context) { + return $this->add_context_by_id($context->id); + } + + /** + * Add a single context to this approved_contextlist by it's ID. + * + * @param int $contextid The context to be added. + * @return $this + */ + public function add_context_by_id($contextid) { + return $this->set_contextids(array_merge($this->get_contextids(), [$contextid])); + } + + /** + * Add a set of contexts to this approved_contextlist. + * + * @param \context[] $contexts The contexts to be added. + * @return $this + */ + public function add_contexts(array $contexts) { + foreach ($contexts as $context) { + $this->add_context($context); + } + } + + /** + * Add a set of contexts to this approved_contextlist by ID. + * + * @param int[] $contexts The contexts to be added. + * @return $this + */ + public function add_contexts_by_id(array $contexts) { + foreach ($contexts as $contextid) { + $this->add_context_by_id($contextid); + } + } +} diff --git a/privacy/classes/tests/request/content_writer.php b/privacy/classes/tests/request/content_writer.php new file mode 100644 index 00000000000..ca185a698af --- /dev/null +++ b/privacy/classes/tests/request/content_writer.php @@ -0,0 +1,484 @@ +. + +/** + * This file contains the moodle format implementation of the content writer. + * + * @package core_privacy + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core_privacy\tests\request; + +defined('MOODLE_INTERNAL') || die(); + +/** + * An implementation of the content_writer for use in unit tests. + * + * This implementation does not export any data but instead stores it in + * structures within the instance which can be easily queried for use + * during unit tests. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class content_writer implements \core_privacy\local\request\content_writer { + /** + * @var \context The context currently being exported. + */ + protected $context = null; + + /** + * @var array The collection of metadata which has been exported. + */ + protected $metadata = []; + + /** + * @var array The data which has been exported. + */ + protected $data = []; + + /** + * @var array The related data which has been exported. + */ + protected $relateddata = []; + + /** + * @var array The list of stored files which have been exported. + */ + protected $files = []; + + /** + * @var array The custom files which have been exported. + */ + protected $customfiles = []; + + /** + * @var array The site-wide user preferences which have been exported. + */ + protected $userprefs = []; + + /** + * Whether any data has been exported at all within the current context. + */ + public function has_any_data() { + $hasdata = !empty($this->data[$this->context->id]); + $hasrelateddata = !empty($this->relateddata[$this->context->id]); + $hasmetadata = !empty($this->metadata[$this->context->id]); + $hasfiles = !empty($this->files[$this->context->id]); + $hascustomfiles = !empty($this->customfiles[$this->context->id]); + $hasuserprefs = !empty($this->userprefs); + + return $hasdata || $hasrelateddata || $hasmetadata || $hasfiles || $hascustomfiles || $hasuserprefs; + } + + /** + * Constructor for the content writer. + * + * Note: The writer_factory must be passed. + * @param \core_privacy\local\request\writer $writer The writer factory. + */ + public function __construct(\core_privacy\local\request\writer $writer) { + } + + /** + * Set the context for the current item being processed. + * + * @param \context $context The context to use + */ + public function set_context(\context $context) : \core_privacy\local\request\content_writer { + $this->context = $context; + + if (empty($this->data[$this->context->id])) { + $this->data[$this->context->id] = []; + } + + if (empty($this->relateddata[$this->context->id])) { + $this->relateddata[$this->context->id] = []; + } + + if (empty($this->metadata[$this->context->id])) { + $this->metadata[$this->context->id] = []; + } + + if (empty($this->files[$this->context->id])) { + $this->files[$this->context->id] = []; + } + + if (empty($this->customfiles[$this->context->id])) { + $this->customfiles[$this->context->id] = []; + } + + return $this; + } + + /** + * Return the current context. + * + * @return \context + */ + public function get_current_context() : \context { + return $this->context; + } + + /** + * Export the supplied data within the current context, at the supplied subcontext. + * + * @param array $subcontext The location within the current context that this data belongs. + * @param \stdClass $data The data to be exported + */ + public function export_data(array $subcontext, \stdClass $data) : \core_privacy\local\request\content_writer { + array_push($subcontext, 'data'); + + $finalcontent = $data; + + while ($pathtail = array_pop($subcontext)) { + $finalcontent = [ + $pathtail => $finalcontent, + ]; + } + + $this->data[$this->context->id] = array_replace_recursive($this->data[$this->context->id], $finalcontent); + + return $this; + } + + /** + * Get all data within the subcontext. + * + * @param array $subcontext The location within the current context that this data belongs. + * @return array The metadata as a series of keys to value + descrition objects. + */ + public function get_data(array $subcontext = []) { + $basepath = $this->data[$this->context->id]; + while ($subpath = array_shift($subcontext)) { + if (isset($basepath[$subpath])) { + $basepath = $basepath[$subpath]; + } else { + return []; + } + } + + if (isset($basepath['data'])) { + return $basepath['data']; + } else { + return []; + } + } + + /** + * Export metadata about the supplied subcontext. + * + * Metadata consists of a key/value pair and a description of the value. + * + * @param array $subcontext The location within the current context that this data belongs. + * @param string $key The metadata name. + * @param string $value The metadata value. + * @param string $description The description of the value. + * @return $this + */ + public function export_metadata(array $subcontext, + string $key, + $value, + string $description + ) : \core_privacy\local\request\content_writer { + array_push($subcontext, 'metadata'); + + $finalcontent = [ + $key => (object) [ + 'value' => $value, + 'description' => $description, + ], + ]; + + while ($pathtail = array_pop($subcontext)) { + $finalcontent = [ + $pathtail => $finalcontent, + ]; + } + + $this->metadata[$this->context->id] = array_replace_recursive($this->metadata[$this->context->id], $finalcontent); + + return $this; + } + + /** + * Get all metadata within the subcontext. + * + * @param array $subcontext The location within the current context that this data belongs. + * @return array The metadata as a series of keys to value + descrition objects. + */ + public function get_all_metadata(array $subcontext = []) { + $basepath = $this->metadata[$this->context->id]; + while ($subpath = array_shift($subcontext)) { + if (isset($basepath[$subpath])) { + $basepath = $basepath[$subpath]; + } + } + + if (isset($basepath['metadata'])) { + return $basepath['metadata']; + } else { + return []; + } + } + + /** + * Get the specified metadata within the subcontext. + * + * @param array $subcontext The location within the current context that this data belongs. + * @param string $key The metadata to be fetched within the context + subcontext. + * @param boolean $valueonly Whether to fetch only the value, rather than the value + description. + * @return array The metadata as a series of keys to value + descrition objects. + */ + public function get_metadata(array $subcontext = [], $key, $valueonly = true) { + $data = $this->get_all_metadata($subcontext); + + if (!isset($data[$key])) { + return null; + } + + $metadata = $data[$key]; + if ($valueonly) { + return $metadata->value; + } else { + return $metadata; + } + } + + /** + * Export a piece of related data. + * + * @param array $subcontext The location within the current context that this data belongs. + * @param string $name The name of the file to be exported. + * @param \stdClass $data The related data to export. + */ + public function export_related_data(array $subcontext, $name, $data) : \core_privacy\local\request\content_writer { + array_push($subcontext, $name); + array_push($subcontext, 'data'); + + $finalcontent = $data; + + while ($pathtail = array_pop($subcontext)) { + $finalcontent = [ + $pathtail => $finalcontent, + ]; + } + + $this->relateddata[$this->context->id] = array_replace_recursive($this->relateddata[$this->context->id], $finalcontent); + + return $this; + } + + /** + * Get all data within the subcontext. + * + * @param array $subcontext The location within the current context that this data belongs. + * @param string $filename The name of the intended filename. + * @return array The metadata as a series of keys to value + descrition objects. + */ + public function get_related_data(array $subcontext = [], $filename) { + $basepath = $this->relateddata[$this->context->id]; + $subcontext[] = $filename; + while ($subpath = array_shift($subcontext)) { + if (isset($basepath[$subpath])) { + $basepath = $basepath[$subpath]; + } else { + return []; + } + } + + if (isset($basepath['data'])) { + return $basepath['data']; + } else { + return []; + } + } + + /** + * Export a piece of data in a custom format. + * + * @param array $subcontext The location within the current context that this data belongs. + * @param string $filename The name of the file to be exported. + * @param string $filecontent The content to be exported. + */ + public function export_custom_file(array $subcontext, $filename, $filecontent) : \core_privacy\local\request\content_writer { + $filename = clean_param($filename, PARAM_FILE); + + $finalcontent = [ + $filename => $filecontent, + ]; + while ($pathtail = array_pop($subcontext)) { + $finalcontent = [ + $pathtail => $finalcontent, + ]; + } + + $this->customfiles[$this->context->id] = array_replace_recursive($this->customfiles[$this->context->id], $finalcontent); + + return $this; + } + + /** + * Get the specified custom file within the subcontext. + * + * @param array $subcontext The location within the current context that this data belongs. + * @param string $filename The name of the file to be fetched within the context + subcontext. + * @return string The content of the file. + */ + public function get_custom_file(array $subcontext = [], $filename = null) { + if (!empty($filename)) { + array_push($subcontext, $filename); + } + + $basepath = $this->customfiles[$this->context->id]; + while ($subpath = array_shift($subcontext)) { + if (isset($basepath[$subpath])) { + $basepath = $basepath[$subpath]; + } + } + + return $basepath; + } + + /** + * Prepare a text area by processing pluginfile URLs within it. + * + * @param array $subcontext The location within the current context that this data belongs. + * @param string $component The name of the component that the files belong to. + * @param string $filearea The filearea within that component. + * @param string $itemid Which item those files belong to. + * @param string $text The text to be processed + * @return string The processed string + */ + public function rewrite_pluginfile_urls(array $subcontext, $component, $filearea, $itemid, $text) : string { + return str_replace('@@PLUGINFILE@@/', 'files/', $text); + } + + /** + * Export all files within the specified component, filearea, itemid combination. + * + * @param array $subcontext The location within the current context that this data belongs. + * @param string $component The name of the component that the files belong to. + * @param string $filearea The filearea within that component. + * @param string $itemid Which item those files belong to. + */ + public function export_area_files(array $subcontext, $component, $filearea, $itemid) : \core_privacy\local\request\content_writer { + $fs = get_file_storage(); + $files = $fs->get_area_files($this->context->id, $component, $filearea, $itemid); + foreach ($files as $file) { + $this->export_file($subcontext, $file); + } + + return $this; + } + + /** + * Export the specified file in the target location. + * + * @param array $subcontext The location within the current context that this data belongs. + * @param \stored_file $file The file to be exported. + */ + public function export_file(array $subcontext, \stored_file $file) : \core_privacy\local\request\content_writer { + if (!$file->is_directory()) { + $subcontextextra = [ + 'files', + $file->get_filepath(), + ]; + $newsubcontext = array_merge($subcontext, $subcontextextra); + + $finalcontent = [ + $file, + ]; + while ($pathtail = array_pop($subcontext)) { + $finalcontent = [ + $pathtail => $finalcontent, + ]; + } + + $this->customfiles[$this->context->id] = array_replace_recursive($this->customfiles[$this->context->id], $finalcontent); + } + + return $this; + } + + /** + * Get all files in the specfied subcontext. + * + * @param array $subcontext The location within the current context that this data belongs. + * @return \stored_file[] The list of stored_files in this context + subcontext. + */ + public function get_files(array $subcontext = []) { + $basepath = $this->files[$this->context->id]; + while ($subpath = array_shift($subcontext)) { + if (isset($basepath[$subpath])) { + $basepath = $basepath[$subpath]; + } + } + + return $basepath; + } + + /** + * Export the specified user preference. + * + * @param string $component The name of the component. + * @param string $key The name of th key to be exported. + * @param string $value The value of the preference + * @param string $description A description of the value + * @return \core_privacy\local\request\content_writer + */ + public function export_user_preference( + string $component, + string $key, + string $value, + string $description + ) : \core_privacy\local\request\content_writer { + if (!isset($this->userprefs[$component])) { + $this->userprefs[$component] = (object) []; + } + + $this->userprefs[$component]->$key = (object) [ + 'value' => $value, + 'description' => $description, + ]; + + return $this; + } + + /** + * Get all user preferences for the specified component. + * + * @param string $component The name of the component. + * @return \stdClass + */ + public function get_user_preferences(string $component) { + if (isset($this->userprefs[$component])) { + return $this->userprefs[$component]; + } else { + return (object) []; + } + } + + /** + * Perform any required finalisation steps and return the location of the finalised export. + * + * @return string + */ + public function finalise_content() : string { + return 'mock_path'; + } +} diff --git a/privacy/tests/approved_contextlist_test.php b/privacy/tests/approved_contextlist_test.php new file mode 100644 index 00000000000..6f4d7d51b63 --- /dev/null +++ b/privacy/tests/approved_contextlist_test.php @@ -0,0 +1,58 @@ +. + +/** + * Unit Tests for the approved contextlist Class + * + * @package core_privacy + * @category test + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +use \core_privacy\local\request\approved_contextlist; + +/** + * Tests for the \core_privacy API's approved contextlist functionality. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class approved_contextlist_test extends advanced_testcase { + /** + * The approved contextlist should not be modifiable once set. + */ + public function test_default_values_set() { + $testuser = \core_user::get_user_by_username('admin'); + $contextids = [3, 2, 1]; + $component = 'core_privacy'; + + $uit = new approved_contextlist($testuser, $component, $contextids); + + $this->assertEquals($testuser, $uit->get_user()); + $this->assertEquals($component, $uit->get_component()); + $result = $uit->get_contextids(); + + // Note: Array order is not guaranteed and should not matter. + foreach ($contextids as $contextid) { + $this->assertNotFalse(array_search($contextid, $result)); + } + } +} diff --git a/privacy/tests/collection_test.php b/privacy/tests/collection_test.php new file mode 100644 index 00000000000..f8a324d7214 --- /dev/null +++ b/privacy/tests/collection_test.php @@ -0,0 +1,211 @@ +. + +/** + * Collection unit tests. + * + * @package core_privacy + * @category test + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +use \core_privacy\local\metadata\collection; +use \core_privacy\local\metadata\types; + +/** + * Tests for the \core_privacy API's collection functionality. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_privacy_metadata_collection extends advanced_testcase { + + + /** + * Test that adding an unknown type causes the type to be added to the collection. + */ + public function test_add_type_generic_type() { + $collection = new collection('core_privacy'); + + // Mock a new types\type. + $mockedtype = $this->createMock(types\type::class); + $collection->add_type($mockedtype); + + $items = $collection->get_collection(); + $this->assertCount(1, $items); + $this->assertEquals($mockedtype, reset($items)); + } + + /** + * Test that adding a known type works as anticipated. + */ + public function test_add_type_known_type() { + $collection = new collection('core_privacy'); + + $linked = new types\subsystem_link('example', 'langstring'); + $collection->add_type($linked); + + $items = $collection->get_collection(); + $this->assertCount(1, $items); + $this->assertEquals($linked, reset($items)); + } + + /** + * Test that adding multiple types returns them all. + */ + public function test_add_type_multiple() { + $collection = new collection('core_privacy'); + + $a = new types\subsystem_link('example', 'langstring'); + $collection->add_type($a); + + $b = new types\subsystem_link('example', 'langstring'); + $collection->add_type($b); + + $items = $collection->get_collection(); + $this->assertCount(2, $items); + } + + /** + * Test that the add_database_table function adds a database table. + */ + public function test_add_database_table() { + $collection = new collection('core_privacy'); + + $name = 'example'; + $fields = ['field' => 'description']; + $summary = 'summarisation'; + + $collection->add_database_table($name, $fields, $summary); + + $items = $collection->get_collection(); + $this->assertCount(1, $items); + $item = reset($items); + $this->assertInstanceOf(types\database_table::class, $item); + $this->assertEquals($name, $item->get_name()); + $this->assertEquals($fields, $item->get_privacy_fields()); + $this->assertEquals($summary, $item->get_summary()); + } + + /** + * Test that the add_user_preference function adds a single user preference. + */ + public function test_add_user_preference() { + $collection = new collection('core_privacy'); + + $name = 'example'; + $summary = 'summarisation'; + + $collection->add_user_preference($name, $summary); + + $items = $collection->get_collection(); + $this->assertCount(1, $items); + $item = reset($items); + $this->assertInstanceOf(types\user_preference::class, $item); + $this->assertEquals($name, $item->get_name()); + $this->assertEquals($summary, $item->get_summary()); + } + + /** + * Test that the link_external_location function links an external location. + */ + public function test_link_external_location() { + $collection = new collection('core_privacy'); + + $name = 'example'; + $fields = ['field' => 'description']; + $summary = 'summarisation'; + + $collection->link_external_location($name, $fields, $summary); + + $items = $collection->get_collection(); + $this->assertCount(1, $items); + $item = reset($items); + $this->assertInstanceOf(types\external_location::class, $item); + $this->assertEquals($name, $item->get_name()); + $this->assertEquals($fields, $item->get_privacy_fields()); + $this->assertEquals($summary, $item->get_summary()); + } + + /** + * Test that the link_subsystem function links the subsystem. + */ + public function test_link_subsystem() { + $collection = new collection('core_privacy'); + + $name = 'example'; + $summary = 'summarisation'; + + $collection->link_subsystem($name, $summary); + + $items = $collection->get_collection(); + $this->assertCount(1, $items); + $item = reset($items); + $this->assertInstanceOf(types\subsystem_link::class, $item); + $this->assertEquals($name, $item->get_name()); + $this->assertEquals($summary, $item->get_summary()); + } + + /** + * Test that the link_plugintype function links the plugin. + */ + public function test_link_plugintype() { + $collection = new collection('core_privacy'); + + $name = 'example'; + $summary = 'summarisation'; + + $collection->link_plugintype($name, $summary); + + $items = $collection->get_collection(); + $this->assertCount(1, $items); + $item = reset($items); + $this->assertInstanceOf(types\plugintype_link::class, $item); + $this->assertEquals($name, $item->get_name()); + $this->assertEquals($summary, $item->get_summary()); + } + + /** + * Data provider to supply a list of valid components. + * + * @return array + */ + public function component_list_provider() { + return [ + ['core_privacy'], + ['mod_forum'], + ]; + } + + /** + * Test that we can get the component correctly. + * + * The component will be used for string translations. + * + * @dataProvider component_list_provider + * @param string $component The component to test + */ + public function test_get_component($component) { + $collection = new collection($component); + + $this->assertEquals($component, $collection->get_component()); + } +} diff --git a/privacy/tests/contextlist_base_test.php b/privacy/tests/contextlist_base_test.php new file mode 100644 index 00000000000..e60dcbcb406 --- /dev/null +++ b/privacy/tests/contextlist_base_test.php @@ -0,0 +1,162 @@ +. + +/** + * Unit Tests for the abstract contextlist Class + * + * @package core_privacy + * @category test + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +use \core_privacy\local\request\contextlist_base; + +/** + * Tests for the \core_privacy API's contextlist base functionality. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class contextlist_base_test extends advanced_testcase { + /** + * Ensure that get_contextids returns the list of unique contextids. + * + * @dataProvider get_contextids_provider + * @param array $input List of context IDs + * @param array $expected list of contextids + * @param int $count Expected count + */ + public function test_get_contextids($input, $expected, $count) { + $uit = new test_contextlist_base(); + $uit->set_contextids($input); + + $result = $uit->get_contextids(); + $this->assertCount($count, $result); + + // Note: Array order is not guaranteed and should not matter. + foreach ($expected as $contextid) { + $this->assertNotFalse(array_search($contextid, $result)); + } + } + + /** + * Provider for the list of contextids. + * + * @return array + */ + public function get_contextids_provider() { + return [ + 'basic' => [ + [1, 2, 3, 4, 5], + [1, 2, 3, 4, 5], + 5, + ], + 'duplicates' => [ + [1, 1, 2, 2, 3, 4, 5], + [1, 2, 3, 4, 5], + 5, + ], + 'Mixed order with duplicates' => [ + [5, 4, 2, 5, 4, 1, 3, 4, 1, 5, 5, 5, 2, 4, 1, 2], + [1, 2, 3, 4, 5], + 5, + ], + ]; + } + + /** + * Ensure that get_contexts returns the correct list of contexts. + */ + public function test_get_contexts() { + global $DB; + + $contexts = []; + $contexts[] = \context_system::instance(); + $contexts[] = \context_user::instance(\core_user::get_user_by_username('admin')->id); + + $ids = []; + foreach ($contexts as $context) { + $ids[] = $context->id; + } + + $uit = new test_contextlist_base(); + $uit->set_contextids($ids); + + $result = $uit->get_contexts(); + $this->assertCount(count($contexts), $result); + foreach ($contexts as $context) { + $this->assertNotFalse(array_search($context, $result)); + } + } + + /** + * Ensure that the contextlist_base is countable. + * + * @dataProvider get_contextids_provider + * @param array $input List of context IDs + * @param array $expected list of contextids + * @param int $count Expected count + */ + public function test_countable($input, $expected, $count) { + $uit = new test_contextlist_base(); + $uit->set_contextids($input); + + $this->assertCount($count, $uit); + } + + /** + * Ensure that the contextlist_base iterates over the set of contexts. + */ + public function test_context_iteration() { + global $DB; + + $allcontexts = $DB->get_records('context'); + $contexts = []; + foreach ($allcontexts as $context) { + $contexts[] = \context::instance_by_id($context->id); + } + + $uit = new test_contextlist_base(); + $uit->set_contextids(array_keys($allcontexts)); + + foreach ($uit as $key => $context) { + $this->assertNotFalse(array_search($context, $contexts)); + } + } +} + +/** + * A test class extending the contextlist_base allowing setting of the + * contextids. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class test_contextlist_base extends contextlist_base { + /** + * Set the contextids for the test class. + * + * @param int[] $contexids The list of contextids to use. + */ + public function set_contextids(array $contextids) { + parent::set_contextids($contextids); + } +} diff --git a/privacy/tests/contextlist_collection_test.php b/privacy/tests/contextlist_collection_test.php new file mode 100644 index 00000000000..d3736dee690 --- /dev/null +++ b/privacy/tests/contextlist_collection_test.php @@ -0,0 +1,175 @@ +. + +/** + * Unit Tests for a the collection of contextlists class + * + * @package core_privacy + * @category test + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +use \core_privacy\local\request\contextlist_collection; +use \core_privacy\local\request\contextlist; +use \core_privacy\local\request\approved_contextlist; + +/** + * Tests for the \core_privacy API's contextlist collection functionality. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class contextlist_collection_test extends advanced_testcase { + /** + * A contextlist_collection should support the contextlist type. + */ + public function test_supports_contextlist() { + $uit = new contextlist_collection(1); + $contextlist = new contextlist(); + $contextlist->set_component('core_privacy'); + $uit->add_contextlist($contextlist); + + $this->assertCount(1, $uit->get_contextlists()); + } + + /** + * A contextlist_collection should support the approved_contextlist type. + */ + public function test_supports_approved_contextlist() { + $uit = new contextlist_collection(1); + $testuser = \core_user::get_user_by_username('admin'); + $contextids = [3, 2, 1]; + $uit->add_contextlist(new approved_contextlist($testuser, 'core_privacy', $contextids)); + + $this->assertCount(1, $uit->get_contextlists()); + } + + /** + * Ensure that get_contextlist_for_component returns the correct contextlist. + */ + public function test_get_contextlist_for_component() { + $uit = new contextlist_collection(1); + $coretests = new contextlist(); + $coretests->set_component('core_tests'); + $uit->add_contextlist($coretests); + + $coreprivacy = new contextlist(); + $coreprivacy->set_component('core_privacy'); + $uit->add_contextlist($coreprivacy); + + // Note: This uses assertSame rather than assertEquals. + // The former checks the actual object, whilst assertEquals only checks that they look the same. + $this->assertSame($coretests, $uit->get_contextlist_for_component('core_tests')); + $this->assertSame($coreprivacy, $uit->get_contextlist_for_component('core_privacy')); + } + + /** + * Ensure that get_contextlist_for_component does not die horribly when querying a non-existent component. + */ + public function test_get_contextlist_for_component_not_found() { + $uit = new contextlist_collection(1); + + $this->assertNull($uit->get_contextlist_for_component('core_tests')); + } + + /** + * Ensure that a duplicate contextlist in the collection throws an Exception. + */ + public function test_duplicate_addition_throws() { + $uit = new contextlist_collection(1); + + $coretests = new contextlist(); + $coretests->set_component('core_tests'); + $uit->add_contextlist($coretests); + + $this->expectException('moodle_exception'); + $uit->add_contextlist($coretests); + } + + /** + * Ensure that the contextlist_collection is countable. + */ + public function test_countable() { + $uit = new contextlist_collection(1); + + $contextlist = new contextlist(); + $contextlist->set_component('test_example'); + $uit->add_contextlist($contextlist); + + $contextlist = new contextlist(); + $contextlist->set_component('test_another'); + $uit->add_contextlist($contextlist); + + $this->assertCount(2, $uit); + } + + /** + * Ensure that the contextlist_collection iterates over the set of contextlists. + */ + public function test_iteration() { + $uit = new contextlist_collection(1); + + $testdata = []; + + $component = 'test_example'; + $contextlist = new contextlist(); + $contextlist->set_component($component); + $uit->add_contextlist($contextlist); + $testdata[$component] = $contextlist; + + $component = 'test_another'; + $contextlist = new contextlist(); + $contextlist->set_component($component); + $uit->add_contextlist($contextlist); + $testdata[$component] = $contextlist; + + $component = 'test_third'; + $contextlist = new contextlist(); + $contextlist->set_component($component); + $uit->add_contextlist($contextlist); + $testdata[$component] = $contextlist; + + foreach ($uit as $component => $list) { + $this->assertEquals($testdata[$component], $list); + } + + $this->assertCount(3, $uit); + } + + /** + * Test that the userid is correctly returned. + */ + public function test_get_userid() { + $uit = new contextlist_collection(1); + + $this->assertEquals(1, $uit->get_userid()); + } + + /** + * Test that an exception is thrown if a contextlist does not contain a component. + */ + public function test_add_without_component() { + $uit = new contextlist_collection(1); + + $this->expectException(moodle_exception::class); + $uit->add_contextlist(new contextlist()); + } +} diff --git a/privacy/tests/contextlist_test.php b/privacy/tests/contextlist_test.php new file mode 100644 index 00000000000..2360b122a16 --- /dev/null +++ b/privacy/tests/contextlist_test.php @@ -0,0 +1,55 @@ +. + +/** + * Unit Tests for the approved contextlist Class + * + * @package core_privacy + * @category test + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +use \core_privacy\local\request\contextlist; + +/** + * Tests for the \core_privacy API's approved contextlist functionality. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class contextlist_test extends advanced_testcase { + + /** + * Ensure that valid SQL results in the relevant contexts being added. + */ + public function test_add_from_sql() { + global $DB; + + $sql = "SELECT c.id FROM {context} c"; + $params = []; + $allcontexts = $DB->get_records_sql($sql, $params); + + $uit = new contextlist(); + $uit->add_from_sql($sql, $params); + + $this->assertCount(count($allcontexts), $uit); + } +} diff --git a/privacy/tests/deletion_criteria_test.php b/privacy/tests/deletion_criteria_test.php new file mode 100644 index 00000000000..55b796bc020 --- /dev/null +++ b/privacy/tests/deletion_criteria_test.php @@ -0,0 +1,56 @@ +. + +/** + * Unit Tests for the request deletion criteria. + * + * @package core_privacy + * @category test + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +use \core_privacy\local\request\deletion_criteria; + +/** + * Tests for the \core_privacy API's request deletion criteria class. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class deletion_criteria_test extends advanced_testcase { + /** + * The get_context function should return the entered context. + */ + public function test_get_context() { + $context = \context_system::instance(); + $uit = new deletion_criteria($context); + $this->assertSame($context, $uit->get_context()); + } + + /** + * The get_context function should return the entered context. + */ + public function test_get_context_user_context() { + $context = \context_user::instance(\core_user::get_user_by_username('admin')->id); + $uit = new deletion_criteria($context); + $this->assertSame($context, $uit->get_context()); + } +} diff --git a/privacy/tests/fixtures/logo.png b/privacy/tests/fixtures/logo.png new file mode 100644 index 00000000000..8c3988c3905 Binary files /dev/null and b/privacy/tests/fixtures/logo.png differ diff --git a/privacy/tests/moodle_content_writer_test.php b/privacy/tests/moodle_content_writer_test.php new file mode 100644 index 00000000000..e08a90ffb66 --- /dev/null +++ b/privacy/tests/moodle_content_writer_test.php @@ -0,0 +1,747 @@ +. + +/** + * Unit Tests for the Moodle Content Writer. + * + * @package core_privacy + * @category test + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +use \core_privacy\local\request\writer; +use \core_privacy\local\request\moodle_content_writer; + +/** + * Tests for the \core_privacy API's moodle_content_writer functionality. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class moodle_content_writer_test extends advanced_testcase { + + /** + * Test that exported data is saved correctly within the system context. + * + * @dataProvider export_data_provider + * @param \stdClass $data Data + */ + public function test_export_data($data) { + $context = \context_system::instance(); + $subcontext = []; + + $writer = $this->get_writer_instance() + ->set_context($context) + ->export_data($subcontext, $data); + + $fileroot = $this->fetch_exported_content($writer); + + $contextpath = $this->get_context_path($context, $subcontext, 'data.json'); + $this->assertTrue($fileroot->hasChild($contextpath)); + + $json = $fileroot->getChild($contextpath)->getContent(); + $expanded = json_decode($json); + $this->assertEquals($data, $expanded); + } + + /** + * Test that exported data is saved correctly for context/subcontext. + * + * @dataProvider export_data_provider + * @param \stdClass $data Data + */ + public function test_export_data_different_context($data) { + $context = \context_user::instance(\core_user::get_user_by_username('admin')->id); + $subcontext = ['sub', 'context']; + + $writer = $this->get_writer_instance() + ->set_context($context) + ->export_data($subcontext, $data); + + $fileroot = $this->fetch_exported_content($writer); + + $contextpath = $this->get_context_path($context, $subcontext, 'data.json'); + $this->assertTrue($fileroot->hasChild($contextpath)); + + $json = $fileroot->getChild($contextpath)->getContent(); + $expanded = json_decode($json); + $this->assertEquals($data, $expanded); + } + + /** + * Test that exported is saved within the correct directory locations. + */ + public function test_export_data_writes_to_multiple_context() { + $subcontext = ['sub', 'context']; + + $systemcontext = \context_system::instance(); + $systemdata = (object) [ + 'belongsto' => 'system', + ]; + $usercontext = \context_user::instance(\core_user::get_user_by_username('admin')->id); + $userdata = (object) [ + 'belongsto' => 'user', + ]; + + $writer = $this->get_writer_instance(); + + $writer + ->set_context($systemcontext) + ->export_data($subcontext, $systemdata); + + $writer + ->set_context($usercontext) + ->export_data($subcontext, $userdata); + + $fileroot = $this->fetch_exported_content($writer); + + $contextpath = $this->get_context_path($systemcontext, $subcontext, 'data.json'); + $this->assertTrue($fileroot->hasChild($contextpath)); + + $json = $fileroot->getChild($contextpath)->getContent(); + $expanded = json_decode($json); + $this->assertEquals($systemdata, $expanded); + + $contextpath = $this->get_context_path($usercontext, $subcontext, 'data.json'); + $this->assertTrue($fileroot->hasChild($contextpath)); + + $json = $fileroot->getChild($contextpath)->getContent(); + $expanded = json_decode($json); + $this->assertEquals($userdata, $expanded); + } + + /** + * Test that multiple writes to the same location cause the latest version to be written. + */ + public function test_export_data_multiple_writes_same_context() { + $subcontext = ['sub', 'context']; + + $systemcontext = \context_system::instance(); + $originaldata = (object) [ + 'belongsto' => 'system', + ]; + + $newdata = (object) [ + 'abc' => 'def', + ]; + + $writer = $this->get_writer_instance(); + + $writer + ->set_context($systemcontext) + ->export_data($subcontext, $originaldata); + + $writer + ->set_context($systemcontext) + ->export_data($subcontext, $newdata); + + $fileroot = $this->fetch_exported_content($writer); + + $contextpath = $this->get_context_path($systemcontext, $subcontext, 'data.json'); + $this->assertTrue($fileroot->hasChild($contextpath)); + + $json = $fileroot->getChild($contextpath)->getContent(); + $expanded = json_decode($json); + $this->assertEquals($newdata, $expanded); + } + + /** + * Data provider for exporting user data. + */ + public function export_data_provider() { + return [ + 'basic' => [ + (object) [ + 'example' => (object) [ + 'key' => 'value', + ], + ], + ], + ]; + } + + /** + * Test that metadata can be set. + * + * @dataProvider export_metadata_provider + * @param string $key Key + * @param string $value Value + * @param string $description Description + */ + public function test_export_metadata($key, $value, $description) { + $context = \context_system::instance(); + $subcontext = ['a', 'b', 'c']; + + $writer = $this->get_writer_instance() + ->set_context($context) + ->export_metadata($subcontext, $key, $value, $description); + + $fileroot = $this->fetch_exported_content($writer); + + $contextpath = $this->get_context_path($context, $subcontext, 'metadata.json'); + $this->assertTrue($fileroot->hasChild($contextpath)); + + $json = $fileroot->getChild($contextpath)->getContent(); + $expanded = json_decode($json); + $this->assertTrue(isset($expanded->$key)); + $this->assertEquals($value, $expanded->$key->value); + $this->assertEquals($description, $expanded->$key->description); + } + + /** + * Test that metadata can be set additively. + */ + public function test_export_metadata_additive() { + $context = \context_system::instance(); + $subcontext = []; + + $writer = $this->get_writer_instance(); + + $writer + ->set_context($context) + ->export_metadata($subcontext, 'firstkey', 'firstvalue', 'firstdescription'); + + $writer + ->set_context($context) + ->export_metadata($subcontext, 'secondkey', 'secondvalue', 'seconddescription'); + + $fileroot = $this->fetch_exported_content($writer); + + $contextpath = $this->get_context_path($context, $subcontext, 'metadata.json'); + $this->assertTrue($fileroot->hasChild($contextpath)); + + $json = $fileroot->getChild($contextpath)->getContent(); + $expanded = json_decode($json); + + $this->assertTrue(isset($expanded->firstkey)); + $this->assertEquals('firstvalue', $expanded->firstkey->value); + $this->assertEquals('firstdescription', $expanded->firstkey->description); + + $this->assertTrue(isset($expanded->secondkey)); + $this->assertEquals('secondvalue', $expanded->secondkey->value); + $this->assertEquals('seconddescription', $expanded->secondkey->description); + } + + /** + * Test that metadata can be set additively. + */ + public function test_export_metadata_to_multiple_contexts() { + $systemcontext = \context_system::instance(); + $usercontext = \context_user::instance(\core_user::get_user_by_username('admin')->id); + $subcontext = []; + + $writer = $this->get_writer_instance(); + + $writer + ->set_context($systemcontext) + ->export_metadata($subcontext, 'firstkey', 'firstvalue', 'firstdescription') + ->export_metadata($subcontext, 'secondkey', 'secondvalue', 'seconddescription'); + + $writer + ->set_context($usercontext) + ->export_metadata($subcontext, 'firstkey', 'alternativevalue', 'alternativedescription') + ->export_metadata($subcontext, 'thirdkey', 'thirdvalue', 'thirddescription'); + + $fileroot = $this->fetch_exported_content($writer); + + $systemcontextpath = $this->get_context_path($systemcontext, $subcontext, 'metadata.json'); + $this->assertTrue($fileroot->hasChild($systemcontextpath)); + + $json = $fileroot->getChild($systemcontextpath)->getContent(); + $expanded = json_decode($json); + + $this->assertTrue(isset($expanded->firstkey)); + $this->assertEquals('firstvalue', $expanded->firstkey->value); + $this->assertEquals('firstdescription', $expanded->firstkey->description); + $this->assertTrue(isset($expanded->secondkey)); + $this->assertEquals('secondvalue', $expanded->secondkey->value); + $this->assertEquals('seconddescription', $expanded->secondkey->description); + $this->assertFalse(isset($expanded->thirdkey)); + + $usercontextpath = $this->get_context_path($usercontext, $subcontext, 'metadata.json'); + $this->assertTrue($fileroot->hasChild($usercontextpath)); + + $json = $fileroot->getChild($usercontextpath)->getContent(); + $expanded = json_decode($json); + + $this->assertTrue(isset($expanded->firstkey)); + $this->assertEquals('alternativevalue', $expanded->firstkey->value); + $this->assertEquals('alternativedescription', $expanded->firstkey->description); + $this->assertFalse(isset($expanded->secondkey)); + $this->assertTrue(isset($expanded->thirdkey)); + $this->assertEquals('thirdvalue', $expanded->thirdkey->value); + $this->assertEquals('thirddescription', $expanded->thirdkey->description); + } + + /** + * Data provider for exporting user metadata. + * + * return array + */ + public function export_metadata_provider() { + return [ + 'basic' => [ + 'key', + 'value', + 'This is a description', + ], + 'valuewithspaces' => [ + 'key', + 'value has mixed', + 'This is a description', + ], + 'encodedvalue' => [ + 'key', + base64_encode('value has mixed'), + 'This is a description', + ], + ]; + } + + /** + * Exporting a single stored_file should cause that file to be output in the files directory. + */ + public function test_export_area_files() { + $this->resetAfterTest(); + $context = \context_system::instance(); + $fs = get_file_storage(); + + // Add two files to core_privacy::tests::0. + $files = []; + $file = (object) [ + 'component' => 'core_privacy', + 'filearea' => 'tests', + 'itemid' => 0, + 'path' => '/', + 'name' => 'a.txt', + 'content' => 'Test file 0', + ]; + $files[] = $file; + + $file = (object) [ + 'component' => 'core_privacy', + 'filearea' => 'tests', + 'itemid' => 0, + 'path' => '/', + 'name' => 'b.txt', + 'content' => 'Test file 1', + ]; + $files[] = $file; + + // One with a different itemid. + $file = (object) [ + 'component' => 'core_privacy', + 'filearea' => 'tests', + 'itemid' => 1, + 'path' => '/', + 'name' => 'c.txt', + 'content' => 'Other', + ]; + $files[] = $file; + + // One with a different filearea. + $file = (object) [ + 'component' => 'core_privacy', + 'filearea' => 'alternative', + 'itemid' => 0, + 'path' => '/', + 'name' => 'd.txt', + 'content' => 'Alternative', + ]; + $files[] = $file; + + // One with a different component. + $file = (object) [ + 'component' => 'core', + 'filearea' => 'tests', + 'itemid' => 0, + 'path' => '/', + 'name' => 'e.txt', + 'content' => 'Other tests', + ]; + $files[] = $file; + + foreach ($files as $file) { + $record = [ + 'contextid' => $context->id, + 'component' => $file->component, + 'filearea' => $file->filearea, + 'itemid' => $file->itemid, + 'filepath' => $file->path, + 'filename' => $file->name, + ]; + + $file->namepath = $file->path . $file->name; + $file->storedfile = $fs->create_file_from_string($record, $file->content); + } + + $writer = $this->get_writer_instance() + ->set_context($context) + ->export_area_files([], 'core_privacy', 'tests', 0); + + $fileroot = $this->fetch_exported_content($writer); + + $firstfiles = array_slice($files, 0, 2); + foreach ($firstfiles as $file) { + $contextpath = $this->get_context_path($context, [get_string('files')], $file->namepath); + $this->assertTrue($fileroot->hasChild($contextpath)); + $this->assertEquals($file->content, $fileroot->getChild($contextpath)->getContent()); + } + + $otherfiles = array_slice($files, 2); + foreach ($otherfiles as $file) { + $contextpath = $this->get_context_path($context, [get_string('files')], $file->namepath); + $this->assertFalse($fileroot->hasChild($contextpath)); + } + } + + /** + * Exporting a single stored_file should cause that file to be output in the files directory. + * + * @dataProvider export_file_provider + * @param string $filepath File path + * @param string $filename File name + * @param string $content Content + */ + public function test_export_file($filepath, $filename, $content) { + $this->resetAfterTest(); + $context = \context_system::instance(); + $filenamepath = $filepath . $filename; + + $filerecord = array( + 'contextid' => $context->id, + 'component' => 'core_privacy', + 'filearea' => 'tests', + 'itemid' => 0, + 'filepath' => $filepath, + 'filename' => $filename, + ); + + $fs = get_file_storage(); + $file = $fs->create_file_from_string($filerecord, $content); + + $writer = $this->get_writer_instance() + ->set_context($context) + ->export_file([], $file); + + $fileroot = $this->fetch_exported_content($writer); + + $contextpath = $this->get_context_path($context, [get_string('files')], $filenamepath); + $this->assertTrue($fileroot->hasChild($contextpath)); + $this->assertEquals($content, $fileroot->getChild($contextpath)->getContent()); + } + + /** + * Data provider for the test_export_file function. + * + * @return array + */ + public function export_file_provider() { + return [ + 'basic' => [ + '/', + 'testfile.txt', + 'An example file content', + ], + 'longpath' => [ + '/path/within/a/path/within/a/path/', + 'testfile.txt', + 'An example file content', + ], + 'pathwithspaces' => [ + '/path with/some spaces/', + 'testfile.txt', + 'An example file content', + ], + 'filewithspaces' => [ + '/path with/some spaces/', + 'test file.txt', + 'An example file content', + ], + 'image' => [ + '/', + 'logo.png', + file_get_contents(__DIR__ . '/fixtures/logo.png'), + ], + 'UTF8' => [ + '/Žluťoučký/', + 'koníček.txt', + 'koníček', + ], + 'EUC-JP' => [ + '/言語設定/', + '言語設定.txt', + '言語設定', + ], + ]; + } + + /** + * User preferences can not be exported against the user context. + */ + public function test_export_user_preference_context_user() { + $admin = \core_user::get_user_by_username('admin'); + + $writer = $this->get_writer_instance(); + + $this->expectException('coding_exception'); + $writer->set_context(\context_user::instance($admin->id)) + ->export_user_preference('core_privacy', 'validkey', 'value', 'description'); + } + + /** + * User preferences can not be exported against the coursecat context. + */ + public function test_export_user_preference_context_coursecat() { + global $DB; + + $categories = $DB->get_records('course_categories'); + $firstcategory = reset($categories); + + $this->expectException('coding_exception'); + $this->get_writer_instance() + ->set_context(\context_coursecat::instance($firstcategory->id)) + ->export_user_preference('core_privacy', 'validkey', 'value', 'description'); + } + + /** + * User preferences can not be exported against the course context. + */ + public function test_export_user_preference_context_course() { + global $DB; + + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + + $this->expectException('coding_exception'); + $this->get_writer_instance() + ->set_context(\context_course::instance($course->id)) + ->export_user_preference('core_privacy', 'validkey', 'value', 'description'); + } + + /** + * User preferences can not be exported against a module context. + */ + public function test_export_user_preference_context_module() { + global $DB; + + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); + + $this->expectException('coding_exception'); + $this->get_writer_instance() + ->set_context(\context_module::instance($forum->cmid)) + ->export_user_preference('core_privacy', 'validkey', 'value', 'description'); + } + + /** + * User preferences can not be exported against a block context. + */ + public function test_export_user_preference_context_block() { + global $DB; + + $blocks = $DB->get_records('block_instances'); + $block = reset($blocks); + + $this->expectException('coding_exception'); + $this->get_writer_instance() + ->set_context(\context_block::instance($block->id)) + ->export_user_preference('core_privacy', 'validkey', 'value', 'description'); + } + + /** + * User preferences can be exported against the system. + * + * @dataProvider export_user_preference_provider + * @param string $component Component + * @param string $key Key + * @param string $value Value + * @param string $desc Description + */ + public function test_export_user_preference_context_system($component, $key, $value, $desc) { + $context = \context_system::instance(); + $writer = $this->get_writer_instance() + ->set_context($context) + ->export_user_preference($component, $key, $value, $desc); + + $fileroot = $this->fetch_exported_content($writer); + + $contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json"); + $this->assertTrue($fileroot->hasChild($contextpath)); + + $json = $fileroot->getChild($contextpath)->getContent(); + $expanded = json_decode($json); + $this->assertTrue(isset($expanded->$key)); + $data = $expanded->$key; + $this->assertEquals($value, $data->value); + $this->assertEquals($desc, $data->description); + } + + /** + * User preferences can be exported against the system. + */ + public function test_export_multiple_user_preference_context_system() { + $context = \context_system::instance(); + $writer = $this->get_writer_instance(); + $component = 'core_privacy'; + + $writer + ->set_context($context) + ->export_user_preference($component, 'key1', 'val1', 'desc1') + ->export_user_preference($component, 'key2', 'val2', 'desc2'); + + $fileroot = $this->fetch_exported_content($writer); + + $contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json"); + $this->assertTrue($fileroot->hasChild($contextpath)); + + $json = $fileroot->getChild($contextpath)->getContent(); + $expanded = json_decode($json); + + $this->assertTrue(isset($expanded->key1)); + $data = $expanded->key1; + $this->assertEquals('val1', $data->value); + $this->assertEquals('desc1', $data->description); + + $this->assertTrue(isset($expanded->key2)); + $data = $expanded->key2; + $this->assertEquals('val2', $data->value); + $this->assertEquals('desc2', $data->description); + } + + /** + * User preferences can be exported against the system. + */ + public function test_export_user_preference_replace() { + $context = \context_system::instance(); + $writer = $this->get_writer_instance(); + $component = 'core_privacy'; + $key = 'key'; + + $writer + ->set_context($context) + ->export_user_preference($component, $key, 'val1', 'desc1'); + + $writer + ->set_context($context) + ->export_user_preference($component, $key, 'val2', 'desc2'); + + $fileroot = $this->fetch_exported_content($writer); + + $contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json"); + $this->assertTrue($fileroot->hasChild($contextpath)); + + $json = $fileroot->getChild($contextpath)->getContent(); + $expanded = json_decode($json); + + $this->assertTrue(isset($expanded->$key)); + $data = $expanded->$key; + $this->assertEquals('val2', $data->value); + $this->assertEquals('desc2', $data->description); + } + + /** + * Provider for various user preferences. + * + * @return array + */ + public function export_user_preference_provider() { + return [ + 'basic' => [ + 'core_privacy', + 'onekey', + 'value', + 'description', + ], + 'encodedvalue' => [ + 'core_privacy', + 'donkey', + base64_encode('value'), + 'description', + ], + 'long description' => [ + 'core_privacy', + 'twokey', + 'value', + 'This is a much longer description which actually states what this is used for. Blah blah blah.', + ], + ]; + } + + /** + * Get a fresh content writer. + * + * @return moodle_content_writer + */ + public function get_writer_instance() { + $factory = $this->createMock(writer::class); + return new moodle_content_writer($factory); + } + + /** + * Fetch the exported content for inspection. + * + * @param moodle_content_writer $writer + * @return \org\bovigo\vfs\vfsStreamDirectory + */ + protected function fetch_exported_content(moodle_content_writer $writer) { + $export = $writer + ->set_context(\context_system::instance()) + ->finalise_content(); + + $fileroot = \org\bovigo\vfs\vfsStream::setup('root'); + + $target = \org\bovigo\vfs\vfsStream::url('root'); + $fp = get_file_packer(); + $fp->extract_to_pathname($export, $target); + + return $fileroot; + } + + /** + * Determine the path for the current context. + * + * Note: This is a wrapper around the real function. + * + * @param \context $context The context being written + * @param array $subcontext The subcontext path + * @param string $name THe name of the file target + * @return array The context path. + */ + protected function get_context_path($context, $subcontext = null, $name = '') { + $rc = new ReflectionClass(moodle_content_writer::class); + $writer = $this->get_writer_instance(); + $writer->set_context($context); + + if (null === $subcontext) { + $rcm = $rc->getMethod('get_context_path'); + $rcm->setAccessible(true); + return $rcm->invoke($writer); + } else { + $rcm = $rc->getMethod('get_path'); + $rcm->setAccessible(true); + return $rcm->invoke($writer, $subcontext, $name); + } + } +} diff --git a/privacy/tests/request_helper_test.php b/privacy/tests/request_helper_test.php new file mode 100644 index 00000000000..b5baf4116d2 --- /dev/null +++ b/privacy/tests/request_helper_test.php @@ -0,0 +1,206 @@ +. + +/** + * Unit Tests for the request helper. + * + * @package core_privacy + * @category test + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +use \core_privacy\local\request\helper; +use \core_privacy\local\request\writer; + +/** + * Tests for the \core_privacy API's request helper functionality. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class request_helper_test extends advanced_testcase { + /** + * Test that basic module data is returned. + */ + public function test_get_context_data_context_module() { + $this->resetAfterTest(); + + // Setup. + $course = $this->getDataGenerator()->create_course(); + $user = \core_user::get_user_by_username('admin'); + + $forum = $this->getDataGenerator()->create_module('forum', [ + 'course' => $course->id, + ]); + $context = context_module::instance($forum->cmid); + $modinfo = get_fast_modinfo($course->id); + $cm = $modinfo->cms[$context->instanceid]; + + // Fetch the data. + $result = helper::get_context_data($context, $user); + $this->assertInstanceOf('stdClass', $result); + + // Check that the name matches. + $this->assertEquals($cm->get_formatted_name(), $result->name); + + // This plugin supports the intro. Check that it is included and correct. + $formattedintro = format_text($forum->intro, $forum->introformat, [ + 'noclean' => true, + 'para' => false, + 'context' => $context, + 'overflowdiv' => true, + ]); + $this->assertEquals($formattedintro, $result->intro); + + // This function should only fetch data. It does not export it. + $this->assertFalse(writer::with_context($context)->has_any_data()); + } + + /** + * Test that basic block data is returned. + */ + public function test_get_context_data_context_block() { + $this->resetAfterTest(); + + // Setup. + $block = $this->getDataGenerator()->create_block('online_users'); + $context = context_block::instance($block->id); + $user = \core_user::get_user_by_username('admin'); + + // Fetch the data. + $data = helper::get_context_data($context, $user); + $this->assertEquals(get_string('pluginname', 'block_online_users'), $data->blocktype); + + // This function should only fetch data. It does not export it. + $this->assertFalse(writer::with_context($context)->has_any_data()); + } + + /** + * Test that a course moudle with completion tracking enabled has the completion data returned. + */ + public function test_get_context_data_context_module_completion() { + $this->resetAfterTest(); + + // Create a module and set completion. + $course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]); + $user = $this->getDataGenerator()->create_user(); + $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student'); + $assign = $this->getDataGenerator()->create_module('assign', ['course' => $course->id, 'completion' => 1]); + $context = context_module::instance($assign->cmid); + $cm = get_coursemodule_from_id('assign', $assign->cmid); + + // Fetch context data. + $contextdata = helper::get_context_data($context, $user); + + // Completion state is zero. + // Check non completion for a user. + $this->assertEquals(0, $contextdata->completion->state); + + // Complete the activity as a user. + $completioninfo = new completion_info($course); + $completioninfo->update_state($cm, COMPLETION_COMPLETE, $user->id); + + // Check that completion is now exported. + $contextdata = helper::get_context_data($context, $user); + $this->assertEquals(1, $contextdata->completion->state); + + // This function should only fetch data. It does not export it. + $this->assertFalse(writer::with_context($context)->has_any_data()); + } + + /** + * Test that when there are no files to export for a course module context, nothing is exported. + */ + public function test_export_context_files_context_module_no_files() { + $this->resetAfterTest(); + + // Setup. + $course = $this->getDataGenerator()->create_course(); + $user = \core_user::get_user_by_username('admin'); + + $forum = $this->getDataGenerator()->create_module('forum', [ + 'course' => $course->id, + ]); + $context = context_module::instance($forum->cmid); + $modinfo = get_fast_modinfo($course->id); + $cm = $modinfo->cms[$context->instanceid]; + + // Fetch the data. + helper::export_context_files($context, $user); + + // This function should only fetch data. It does not export it. + $this->assertFalse(writer::with_context($context)->has_any_data()); + } + + /** + * Test that when there are no files to export for a course context, nothing is exported. + */ + public function test_export_context_files_context_course_no_files() { + $this->resetAfterTest(); + + // Setup. + $course = $this->getDataGenerator()->create_course(); + $user = \core_user::get_user_by_username('admin'); + $context = context_course::instance($course->id); + + // Fetch the data. + helper::export_context_files($context, $user); + + // This function should only fetch data. It does not export it. + $this->assertFalse(writer::with_context($context)->has_any_data()); + } + + /** + * Test that when there are files to export for a course context, the files are exported. + */ + public function test_export_context_files_context_course_intro_files() { + $this->resetAfterTest(); + + // Setup. + $course = $this->getDataGenerator()->create_course(); + $user = \core_user::get_user_by_username('admin'); + $assign = $this->getDataGenerator()->create_module('assign', ['course' => $course->id]); + $context = context_module::instance($assign->cmid); + + // File details. + $filerecord = array( + 'contextid' => $context->id, + 'component' => 'mod_assign', + 'filearea' => 'intro', + 'itemid' => 0, + 'filepath' => '/', + 'filename' => 'logo.png', + ); + + $content = file_get_contents(__DIR__ . '/fixtures/logo.png'); + + // Store the file. + $fs = get_file_storage(); + $file = $fs->create_file_from_string($filerecord, $content); + + // Fetch the data. + helper::export_context_files($context, $user); + + // This should have resulted in the file being exported. + $this->assertTrue(writer::with_context($context)->has_any_data()); + } + +} diff --git a/privacy/tests/request_transform_test.php b/privacy/tests/request_transform_test.php new file mode 100644 index 00000000000..15876adc19b --- /dev/null +++ b/privacy/tests/request_transform_test.php @@ -0,0 +1,110 @@ +. + +/** + * Unit Tests for the request transform helper. + * + * @package core_privacy + * @category test + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +use \core_privacy\local\request\transform; + +/** + * Tests for the \core_privacy API's request transform helper functionality. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class request_transform_test extends advanced_testcase { + /** + * Test that user translation currently does nothing. + * + * We have not determined if we will do this or not, but we provide the functionality and encourgae people to use + * it so that it can be retrospectively fitted if required. + */ + public function test_user() { + // Note: This test currently sucks, but there's no point creating users just to test this. + for ($i = 0; $i < 10; $i++) { + $this->assertEquals($i, transform::user($i)); + } + } + + /** + * Test that the datetime is translated into a string. + */ + public function test_datetime() { + $this->assertInternalType('string', transform::datetime(1)); + } + + /** + * Test that the date is translated into a string. + */ + public function test_date() { + $this->assertInternalType('string', transform::date(1)); + } + + /** + * Ensure that the yesno function translates correctly. + * + * @dataProvider yesno_provider + * @param mixed $input The input to test + * @param string $expected The expected value + */ + public function test_yesno($input, $expected) { + $this->assertEquals($expected, transform::yesno($input)); + } + + /** + * Data provider for tests of the yesno transformation. + * + * @return array + */ + public function yesno_provider() { + return [ + 'Bool False' => [ + false, + get_string('no'), + ], + 'Bool true' => [ + true, + get_string('yes'), + ], + 'Int 0' => [ + 0, + get_string('no'), + ], + 'Int 1' => [ + 1, + get_string('yes'), + ], + 'String 0' => [ + '0', + get_string('no'), + ], + 'String 1' => [ + '1', + get_string('yes'), + ], + ]; + } +} diff --git a/privacy/tests/types_database_table_test.php b/privacy/tests/types_database_table_test.php new file mode 100644 index 00000000000..79ee5e736f8 --- /dev/null +++ b/privacy/tests/types_database_table_test.php @@ -0,0 +1,144 @@ +. + +/** + * Type unit tests for the Database Table. + * + * @package core_privacy + * @category test + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +use \core_privacy\local\metadata\types\database_table; + +/** + * Tests for the \core_privacy API's types\database_table functionality. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_privacy_metadata_types_database_table extends advanced_testcase { + + /** + * Ensure that warnings are thrown if string identifiers contain invalid characters. + * + * @dataProvider invalid_string_provider + * @param string $name Name + * @param array $fields List of fields + * @param string $summary Summary + */ + public function test_invalid_configs($name, $fields, $summary) { + $record = new database_table($name, $fields, $summary); + $this->assertDebuggingCalled(); + } + + /** + * Ensure that warnings are not thrown if debugging is not enabled, even if string identifiers contain invalid characters. + * + * @dataProvider invalid_string_provider + * @param string $name Name + * @param array $fields List of fields + * @param string $summary Summary + */ + public function test_invalid_configs_debug_normal($name, $fields, $summary) { + global $CFG; + $this->resetAfterTest(); + + $CFG->debug = DEBUG_NORMAL; + $record = new database_table($name, $fields, $summary); + $this->assertDebuggingNotCalled(); + } + + /** + * Ensure that no warnings are shown for valid combinations. + * + * @dataProvider valid_string_provider + * @param string $name Name + * @param array $fields List of fields + * @param string $summary Summary + */ + public function test_valid_configs($name, $fields, $summary) { + $record = new database_table($name, $fields, $summary); + $this->assertDebuggingNotCalled(); + } + + /** + * Data provider with a list of invalid string identifiers. + * + * @return array + */ + public function invalid_string_provider() { + return [ + 'Space in summary' => [ + 'example', + [ + 'field' => 'privacy:valid', + ], + 'This table is used for purposes.', + ], + 'Comma in summary' => [ + 'example', + [ + 'field' => 'privacy:valid', + ], + 'privacy,foo', + ], + 'Space in field name' => [ + 'example', + [ + 'field' => 'This field is used for purposes.', + ], + 'privacy:valid', + ], + 'Comma in field name' => [ + 'example', + [ + 'field' => 'invalid,name', + ], + 'privacy:valid', + ], + 'No fields specified' => [ + 'example', + [], + 'privacy:example:valid', + ], + + ]; + } + + /** + * Data provider with a list of valid string identifiers. + * + * @return array + */ + public function valid_string_provider() { + return [ + 'Valid combination' => [ + 'example', + [ + 'field' => 'privacy:example:valid:field', + 'field2' => 'privacy:example:valid:field2', + ], + 'privacy:example:valid', + ], + ]; + } +} diff --git a/privacy/tests/types_external_location_test.php b/privacy/tests/types_external_location_test.php new file mode 100644 index 00000000000..698c4953ed7 --- /dev/null +++ b/privacy/tests/types_external_location_test.php @@ -0,0 +1,144 @@ +. + +/** + * Type unit tests for the External Location. + * + * @package core_privacy + * @category test + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +use \core_privacy\local\metadata\types\external_location; + +/** + * Tests for the \core_privacy API's types\external_location functionality. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_privacy_metadata_types_external_location extends advanced_testcase { + + /** + * Ensure that warnings are thrown if string identifiers contain invalid characters. + * + * @dataProvider invalid_string_provider + * @param string $name Name + * @param array $fields List of fields + * @param string $summary Summary + */ + public function test_invalid_configs($name, $fields, $summary) { + $record = new external_location($name, $fields, $summary); + $this->assertDebuggingCalled(); + } + + /** + * Ensure that warnings are not thrown if debugging is not enabled, even if string identifiers contain invalid characters. + * + * @dataProvider invalid_string_provider + * @param string $name Name + * @param array $fields List of fields + * @param string $summary Summary + */ + public function test_invalid_configs_debug_normal($name, $fields, $summary) { + global $CFG; + $this->resetAfterTest(); + + $CFG->debug = DEBUG_NORMAL; + $record = new external_location($name, $fields, $summary); + $this->assertDebuggingNotCalled(); + } + + /** + * Ensure that no warnings are shown for valid combinations. + * + * @dataProvider valid_string_provider + * @param string $name Name + * @param array $fields List of fields + * @param string $summary Summary + */ + public function test_valid_configs($name, $fields, $summary) { + $record = new external_location($name, $fields, $summary); + $this->assertDebuggingNotCalled(); + } + + /** + * Data provider with a list of invalid string identifiers. + * + * @return array + */ + public function invalid_string_provider() { + return [ + 'Space in summary' => [ + 'example', + [ + 'field' => 'privacy:valid', + ], + 'This table is used for purposes.', + ], + 'Comma in summary' => [ + 'example', + [ + 'field' => 'privacy:valid', + ], + 'privacy,foo', + ], + 'Space in field name' => [ + 'example', + [ + 'field' => 'This field is used for purposes.', + ], + 'privacy:valid', + ], + 'Comma in field name' => [ + 'example', + [ + 'field' => 'invalid,name', + ], + 'privacy:valid', + ], + 'No fields specified' => [ + 'example', + [], + 'privacy:example:valid', + ], + + ]; + } + + /** + * Data provider with a list of valid string identifiers. + * + * @return array + */ + public function valid_string_provider() { + return [ + 'Valid combination' => [ + 'example', + [ + 'field' => 'privacy:example:valid:field', + 'field2' => 'privacy:example:valid:field2', + ], + 'privacy:example:valid', + ], + ]; + } +} diff --git a/privacy/tests/types_plugintype_link_test.php b/privacy/tests/types_plugintype_link_test.php new file mode 100644 index 00000000000..fcb43bf21ad --- /dev/null +++ b/privacy/tests/types_plugintype_link_test.php @@ -0,0 +1,111 @@ +. + +/** + * Types unit tests for the Plugintype Link. + * + * @package core_privacy + * @category test + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +use \core_privacy\local\metadata\types\plugintype_link; + +/** + * Tests for the \core_privacy API's types\plugintype_link functionality. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_privacy_metadata_types_plugintype_link extends advanced_testcase { + + /** + * Ensure that warnings are thrown if string identifiers contain invalid characters. + * + * @dataProvider invalid_string_provider + * @param string $name Name + * @param string $summary Summary + */ + public function test_invalid_configs($name, $summary) { + $record = new plugintype_link($name, $summary); + $this->assertDebuggingCalled(); + } + + /** + * Ensure that warnings are not thrown if debugging is not enabled, even if string identifiers contain invalid characters. + * + * @dataProvider invalid_string_provider + * @param string $name Name + * @param string $summary Summary + */ + public function test_invalid_configs_debug_normal($name, $summary) { + global $CFG; + $this->resetAfterTest(); + + $CFG->debug = DEBUG_NORMAL; + $record = new plugintype_link($name, $summary); + $this->assertDebuggingNotCalled(); + } + + /** + * Ensure that no warnings are shown for valid combinations. + * + * @dataProvider valid_string_provider + * @param string $name Name + * @param string $summary Summary + */ + public function test_valid_configs($name, $summary) { + $record = new plugintype_link($name, $summary); + $this->assertDebuggingNotCalled(); + } + + /** + * Data provider with a list of invalid string identifiers. + * + * @return array + */ + public function invalid_string_provider() { + return [ + 'Space in summary' => [ + 'example', + 'This table is used for purposes.', + ], + 'Comma in summary' => [ + 'example', + 'privacy,foo', + ], + ]; + } + + /** + * Data provider with a list of valid string identifiers. + * + * @return array + */ + public function valid_string_provider() { + return [ + 'Valid combination' => [ + 'example', + 'privacy:example:valid', + ], + ]; + } +} diff --git a/privacy/tests/types_subsystem_link_test.php b/privacy/tests/types_subsystem_link_test.php new file mode 100644 index 00000000000..9a6438d70b9 --- /dev/null +++ b/privacy/tests/types_subsystem_link_test.php @@ -0,0 +1,111 @@ +. + +/** + * Types unit tests for the Subsystem Link. + * + * @package core_privacy + * @category test + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +use \core_privacy\local\metadata\types\subsystem_link; + +/** + * Tests for the \core_privacy API's types\subsystem_link functionality. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_privacy_metadata_types_subsystem_link extends advanced_testcase { + + /** + * Ensure that warnings are thrown if string identifiers contain invalid characters. + * + * @dataProvider invalid_string_provider + * @param string $name Name + * @param string $summary Summary + */ + public function test_invalid_configs($name, $summary) { + $record = new subsystem_link($name, $summary); + $this->assertDebuggingCalled(); + } + + /** + * Ensure that warnings are not thrown if debugging is not enabled, even if string identifiers contain invalid characters. + * + * @dataProvider invalid_string_provider + * @param string $name Name + * @param string $summary Summary + */ + public function test_invalid_configs_debug_normal($name, $summary) { + global $CFG; + $this->resetAfterTest(); + + $CFG->debug = DEBUG_NORMAL; + $record = new subsystem_link($name, $summary); + $this->assertDebuggingNotCalled(); + } + + /** + * Ensure that no warnings are shown for valid combinations. + * + * @dataProvider valid_string_provider + * @param string $name Name + * @param string $summary Summary + */ + public function test_valid_configs($name, $summary) { + $record = new subsystem_link($name, $summary); + $this->assertDebuggingNotCalled(); + } + + /** + * Data provider with a list of invalid string identifiers. + * + * @return array + */ + public function invalid_string_provider() { + return [ + 'Space in summary' => [ + 'example', + 'This table is used for purposes.', + ], + 'Comma in summary' => [ + 'example', + 'privacy,foo', + ], + ]; + } + + /** + * Data provider with a list of valid string identifiers. + * + * @return array + */ + public function valid_string_provider() { + return [ + 'Valid combination' => [ + 'example', + 'privacy:example:valid', + ], + ]; + } +} diff --git a/privacy/tests/types_user_preference_test.php b/privacy/tests/types_user_preference_test.php new file mode 100644 index 00000000000..87f65c51950 --- /dev/null +++ b/privacy/tests/types_user_preference_test.php @@ -0,0 +1,111 @@ +. + +/** + * Types unit tests for the Subsystem Link. + * + * @package core_privacy + * @category test + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +use \core_privacy\local\metadata\types\user_preference; + +/** + * Tests for the \core_privacy API's types\user_preference functionality. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_privacy_metadata_types_user_preference extends advanced_testcase { + + /** + * Ensure that warnings are thrown if string identifiers contain invalid characters. + * + * @dataProvider invalid_string_provider + * @param string $name Name + * @param string $summary Summary + */ + public function test_invalid_configs($name, $summary) { + $record = new user_preference($name, $summary); + $this->assertDebuggingCalled(); + } + + /** + * Ensure that warnings are not thrown if debugging is not enabled, even if string identifiers contain invalid characters. + * + * @dataProvider invalid_string_provider + * @param string $name Name + * @param string $summary Summary + */ + public function test_invalid_configs_debug_normal($name, $summary) { + global $CFG; + $this->resetAfterTest(); + + $CFG->debug = DEBUG_NORMAL; + $record = new user_preference($name, $summary); + $this->assertDebuggingNotCalled(); + } + + /** + * Ensure that no warnings are shown for valid combinations. + * + * @dataProvider valid_string_provider + * @param string $name Name + * @param string $summary Summary + */ + public function test_valid_configs($name, $summary) { + $record = new user_preference($name, $summary); + $this->assertDebuggingNotCalled(); + } + + /** + * Data provider with a list of invalid string identifiers. + * + * @return array + */ + public function invalid_string_provider() { + return [ + 'Space in summary' => [ + 'example', + 'This table is used for purposes.', + ], + 'Comma in summary' => [ + 'example', + 'privacy,foo', + ], + ]; + } + + /** + * Data provider with a list of valid string identifiers. + * + * @return array + */ + public function valid_string_provider() { + return [ + 'Valid combination' => [ + 'example', + 'privacy:example:valid', + ], + ]; + } +} diff --git a/privacy/tests/writer_test.php b/privacy/tests/writer_test.php new file mode 100644 index 00000000000..ebef5b9b221 --- /dev/null +++ b/privacy/tests/writer_test.php @@ -0,0 +1,81 @@ +. + +/** + * Unit Tests for the Moodle Content Writer. + * + * @package core_privacy + * @category test + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +use \core_privacy\local\request\writer; + +/** + * Tests for the \core_privacy API's moodle_content_writer functionality. + * + * Note: The \core_privacy\tests\request\content_writer will be used for these tests. + * This content writer has additional sugar methods for fetching infromation which are not part of the standard + * content_writer interface. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class writer_test extends advanced_testcase { + /** + * Test that calling with_context multiple times will return the same write instance. + */ + public function test_with_context() { + $writer = writer::with_context(\context_system::instance()); + + $this->assertSame($writer, writer::with_context(\context_system::instance())); + } + + /** + * Test that calling with_context multiple times will return the same write instance. + */ + public function test_with_context_different_context_same_instance() { + $writer = writer::with_context(\context_system::instance()); + + $this->assertSame($writer, writer::with_context(\context_user::instance(\core_user::get_user_by_username('admin')->id))); + } + + /** + * Test that calling writer::reset() causes a new copy of the writer to be returned. + */ + public function test_reset() { + $writer = writer::with_context(\context_system::instance()); + writer::reset(); + + $this->assertNotSame($writer, writer::with_context(\context_system::instance())); + } + + /** + * Test that the export_user_preference calls the writer against the system context. + */ + public function test_export_user_preference_sets_system_context() { + $writer = writer::with_context(\context_user::instance(\core_user::get_user_by_username('admin')->id)); + + writer::export_user_preference('core_test', 'key', 'value', 'description'); + + $this->assertSame(\context_system::instance(), $writer->get_current_context()); + } +}