Skip to content

Instantly share code, notes, and snippets.

@luukverhoeven
Last active January 9, 2025 18:39
Show Gist options
  • Save luukverhoeven/c53e5585a9d059cf61537f6d7f37863a to your computer and use it in GitHub Desktop.
Save luukverhoeven/c53e5585a9d059cf61537f6d7f37863a to your computer and use it in GitHub Desktop.
Moodle privacy testing tool
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Privacy provider tester
*
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*
* @package mod_coursecontrol
* @copyright 03/02/2022 Mfreak.nl | LdesignMedia.nl - Luuk Verhoeven
* @author Vincent Cornelis
**/
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
define('CLI_SCRIPT', true);
require(__DIR__ . '/../../config.php');
$plugin = new stdClass();
require('version.php');
$action = $argv[1] ?? false;
$userids = array_slice($argv, 2);
mtrace('First supplied userid is used for single user tests.');
$userid = $userids[0] ?? 2;
if (empty($action)) {
die('Usage privacy-test.php [export|test|delete] userid (int)');
}
if (empty($userid)) {
die('argv 2 must be one or more Userid(s) [not provided]');
}
function check_implements($component, $interface) {
$manager = new \core_privacy\manager();
$rc = new \ReflectionClass(\core_privacy\manager::class);
$rcm = $rc->getMethod('component_implements');
$rcm->setAccessible(true);
return $rcm->invoke($manager, $component, $interface);
}
$provider = '\\' . $plugin->component . '\\privacy\\provider';
$user = \core_user::get_user($userid);
if (empty($user)) {
mtrace("User not found");
return;
}
\core\session\manager::init_empty_session();
\core\session\manager::set_user($user);
// Get approvedlist $approvedlist.
$manager = new \core_privacy\manager();
$approvedlist = new \core_privacy\local\request\contextlist_collection($user->id);
$contextlists = $manager->get_contexts_for_userid($user->id);
foreach ($contextlists as $contextlist) {
$approvedlist->add_contextlist(new \core_privacy\local\request\approved_contextlist(
$user,
$contextlist->get_component(),
$contextlist->get_contextids()
));
}
if ($action == 'test') {
mtrace("TEST ");
$rc = new \ReflectionClass(\core_privacy\manager::class);
$rcm = $rc->getMethod('get_component_list');
$rcm->setAccessible(true);
$manager = new \core_privacy\manager();
$components = $rcm->invoke($manager);
$list = (object) [
'good' => [],
'bad' => [],
];
foreach ($components as $component) {
if ($component !== $plugin->component) {
continue;
}
$compliant = $manager->component_is_compliant($component);
if ($compliant) {
$list->good[] = $component;
} else {
$list->bad[] = $component;
}
}
if (!empty($list->bad)) {
mtrace("The following plugins are not compliant:\n");
mtrace("=> " . implode("\n=> ", array_values($list->bad)) . "\n");
}
mtrace("\n");
mtrace("Testing the compliant plugins:\n");
foreach ($list->good as $component) {
$classname = \core_privacy\manager::get_provider_classname_for_component($component);
mtrace("== {$component} ($classname) ==\n");
if (check_implements($component, \core_privacy\local\metadata\null_provider::class)) {
mtrace(" Claims not to store any data with reason:\n");
mtrace(" '" . get_string($classname::get_reason(), $component) . "'\n");
} else if (check_implements($component, \core_privacy\local\metadata\provider::class)) {
$collection = new \core_privacy\local\metadata\collection($component);
$classname::get_metadata($collection);
$collectionlist = $collection->get_collection();
$count = count($collectionlist);
mtrace(" Found {$count} items of metadata\n");
if (empty($count)) {
mtrace("!!! No metadata found!!! This an error.\n");
}
if (check_implements($component, \core_privacy\local\request\user_preference_provider::class)) {
$userprefdescribed = false;
foreach ($collection->get_collection() as $item) {
if ($item instanceof \core_privacy\local\metadata\types\user_preference) {
$userprefdescribed = true;
mtrace(" " . $item->get_name() . " : " . get_string($item->get_summary(), $component) . "\n");
}
}
if (!$userprefdescribed) {
mtrace("!!! User preference found, but was not described in metadata\n");
}
}
if (check_implements($component, \core_privacy\local\request\core_user_data_provider::class)) {
// No need to check the return type - it's enforced by the interface.
$contextlist = $classname::get_contexts_for_userid($user->id);
$approvedcontextlist =
new \core_privacy\local\request\approved_contextlist($user, $contextlist->get_component(),
$contextlist->get_contextids());
if (count($approvedcontextlist)) {
$classname::export_user_data($approvedcontextlist);
mtrace(" Successfully ran a test export\n");
} else {
mtrace(" Nothing to export.\n");
}
}
if (check_implements($component, \core_privacy\local\request\shared_data_provider::class)) {
mtrace(" This is a shared data provider\n");
}
}
}
mtrace("\n\n== Done ==\n");
} else if ($action == 'export') {
mtrace("EXPORT TEST");
$exportedcontent = $manager->export_user_data($approvedlist);
mtrace("\n");
mtrace("== File was successfully exported to {$exportedcontent}\n");
$basedir = make_temp_directory('privacy');
$exportpath = make_unique_writable_directory($basedir, true);
$fp = get_file_packer();
$fp->extract_to_pathname($exportedcontent, $exportpath);
mtrace("== File export was uncompressed to {$exportpath}\n");
mtrace("============================================================================\n");
} else if ($action == 'delete') {
mtrace("DELETE TEST");
$contextlist = $provider::get_contexts_for_userid($user->id);
$contexts = $contextlist->get_contexts();
$DB->set_debug(true);
$transaction = $DB->start_delegated_transaction();
if (count($contexts) === 0) {
$transaction->rollback(new moodle_exception('Error - no contexts'));
}
if (count($userids) === 0) {
foreach ($contexts as $context) {
mtrace("DELETE data for all users in context {$context->id}");
$provider::delete_data_for_all_users_in_context($context);
}
}
if (count($userids) === 1) {
mtrace("DELETE data for single user: " . $user->id);
/** @var \core_privacy\local\request\approved_contextlist $approved */
foreach ($approvedlist as $approved) {
if ($approved->get_component() == $plugin->component) {
// Test delete all users content by context.
$provider::delete_data_for_user($approved);
}
}
}
if (count($userids) > 1) {
mtrace("DELETE data for user ids: " . implode(', ', $userids));
foreach ($contexts as $context) {
$userlist = new \core_privacy\local\request\approved_userlist($context, $plugin->component, $userids);
$provider::delete_data_for_users($userlist);
}
}
$transaction->rollback(new moodle_exception('Test completed'));
}
@luukverhoeven
Copy link
Author

Usage from cli:

# Usage
# 2 = userid
privacy-test.php export 2
privacy-test.php test 2
privacy-test.php delete 2

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment