diff --git a/dta.zip b/dta.zip index 1dc2cab2e4b76667b091b2d6c98f629ee9e195a3..afca1bf23f47123d6decf766493a73598665084d 100644 Binary files a/dta.zip and b/dta.zip differ diff --git a/dta/models/DtaResult.php b/dta/models/DtaResult.php index 85e1ee7ac854335464a7604f502be043f3cc366c..a06e33d2976082a5c7c3280d70fa67a020761842 100644 --- a/dta/models/DtaResult.php +++ b/dta/models/DtaResult.php @@ -69,7 +69,7 @@ class DtaResult { /** * @var $failurereason Reason of test failure if applicable, "" otherwise. - */ + */ public $failurereason; /** @@ -119,7 +119,7 @@ class DtaResultSummary { /** * @var $timestamp Result timestamp for chronological ordering and deletion of previous results. - */ + */ public $timestamp; /** diff --git a/dta/privacy/provider.php b/dta/privacy/provider.php new file mode 100644 index 0000000000000000000000000000000000000000..7f30ade97fe221410ec11be12d0f2e4d215f26ca --- /dev/null +++ b/dta/privacy/provider.php @@ -0,0 +1,264 @@ +<?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/>. + +/** + * provider for data privacy + * + * @package assignsubmission_dta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright Gero Lueckemeyer and student project teams + */ +namespace assignsubmission_dta\privacy; + +defined('MOODLE_INTERNAL') || die(); + +use core_privacy\local\metadata\collection; +use \core_privacy\local\request\writer; +use \core_privacy\local\request\contextlist; +use \mod_assign\privacy\assign_plugin_request_data; + +class provider implements + // This plugin does store personal user data. + \core_privacy\local\metadata\provider, + \mod_assign\privacy\assignsubmission_provider, + \mod_assign\privacy\assignsubmission_user_provider + { + + /** + * File area for dta submission assignment. + */ + const ASSIGNSUBMISSION_DTA_FILEAREA_SUBMISSION = "submissions_dta"; + + public static function get_metadata(collection $collection): collection { + $collection->add_subsystem_link( + 'core_files', + [], + 'privacy:metadata:core_files' + ); + + $collection->add_database_table( + 'assignsubmission_dta_summary', + [ + 'assignmentid' => 'privacy:metadata:assignsubmission_dta_summary:assignmentid', + 'submissionid' => 'privacy:metadata:assignsubmission_dta_summary:submissionid', + 'timestamp' => 'privacy:metadata:assignsubmission_dta_summary:timestamp', + 'global_stacktrace' => 'privacy:metadata:assignsubmission_dta_summary:global_stacktrace', + 'successful_competencies' => 'privacy:metadata:assignsubmission_dta_summary:successful_competencies', + 'tested_competencies' => 'privacy:metadata:assignsubmission_dta_summary:tested_competencies', + ], + 'privacy:metadata:assignsubmission_dta_summary' + ); + + $collection->add_database_table( + 'assignsubmission_dta_result', + [ + 'assignmentid' => 'privacy:metadata:assignsubmission_dta_result:assignmentid', + 'submissionid' => 'privacy:metadata:assignsubmission_dta_result:submissionid', + 'package_name' => 'privacy:metadata:assignsubmission_dta_result:package_name', + 'class_name' => 'privacy:metadata:assignsubmission_dta_result:class_name', + 'name' => 'privacy:metadata:assignsubmission_dta_result:name', + 'state' => 'privacy:metadata:assignsubmission_dta_result:state', + 'failure_type' => 'privacy:metadata:assignsubmission_dta_result:failure_type', + 'failure_reason' => 'privacy:metadata:assignsubmission_dta_result:failure_reason', + 'stacktrace' => 'privacy:metadata:assignsubmission_dta_result:stacktrace', + 'column_number' => 'privacy:metadata:assignsubmission_dta_result:column_number', + 'line_number' => 'privacy:metadata:assignsubmission_dta_result:line_number', + 'position' => 'privacy:metadata:assignsubmission_dta_result:position', + ], + 'privacy:metadata:assignsubmission_dta_result' + ); + + $collection->add_external_location_link('dta_backend', [ + 'assignmentid' => 'privacy:metadata:assignsubmission_dta_result:assignmentid', + 'submissionid' => 'privacy:metadata:assignsubmission_dta_result:submissionid', + 'submissioncontent' => 'privacy:metadata:core_files', + ], + 'privacy:metadata:dta_backend' + ); + + return $collection; + } + + /** + * This is covered by mod_assign provider and the query on assign_submissions. + * + * @param int $userid The user ID that we are finding contexts for. + * @param contextlist $contextlist A context list to add sql and params to for contexts. + */ + public static function get_context_for_userid_within_submission(int $userid, contextlist $contextlist) { + // This is already fetched from mod_assign. + } + + /** + * This is also covered by the mod_assign provider and its queries. + * + * @param \mod_assign\privacy\useridlist $useridlist An object for obtaining user IDs of students. + */ + public static function get_student_user_ids(\mod_assign\privacy\useridlist $useridlist) { + // This is already fetched from mod_assign. + } + + /** + * If you have tables that contain userids and you can generate entries in your tables without creating an + * entry in the assign_submission table then please fill in this method. + * + * @param userlist $userlist The userlist object + */ + public static function get_userids_from_context(\core_privacy\local\request\userlist $userlist) { + // Not required. + } + + /** + * Export all user data for this plugin. + * + * @param assign_plugin_request_data $exportdata Data used to determine which context and user to export and other useful + * information to help with exporting. + */ + public static function export_submission_user_data(assign_plugin_request_data $exportdata) { + // We currently don't show submissions to teachers when exporting their data. + $context = $exportdata->get_context(); + if ($exportdata->get_user() != null) { + return null; + } + $user = new \stdClass(); + $assign = $exportdata->get_assign(); + $submission = $exportdata->get_pluginobject(); + $files = get_files($submission, $user); + foreach ($files as $file) { + $userid = $exportdata->get_pluginobject()->userid; + $dtaresultsummary=DBUtils::getresultsummaryfromdatabase($assign->id, $submission->id); + // Submitted file. + writer::with_context($exportdata->get_context())->export_file($exportdata->get_subcontext(), $file) + // DTA result. + ->export_related_data($dtaresultsummary); + + // Plagiarism data. + $coursecontext = $context->get_course_context(); + \core_plagiarism\privacy\provider::export_plagiarism_user_data($userid, $context, $exportdata->get_subcontext(), [ + 'cmid' => $context->instanceid, + 'course' => $coursecontext->instanceid, + 'userid' => $userid, + 'file' => $file + ]); + } + } + + /** + * Any call to this method should delete all user data for the context defined in the deletion_criteria. + * + * @param assign_plugin_request_data $requestdata Information useful for deleting user data. + */ + public static function delete_submission_for_context(assign_plugin_request_data $requestdata) { + global $DB; + + \core_plagiarism\privacy\provider::delete_plagiarism_for_context($requestdata->get_context()); + + $fs = get_file_storage(); + $fs->delete_area_files($requestdata->get_context()->id, 'assignsubmission_dta', ASSIGNSUBMISSION_DTA_FILEAREA_SUBMISSION); + + $assignmentid = $requestdata->get_assign()->get_instance()->id; + + // Delete records from assignsubmission_dta tables. + $DB->delete_records('assignsubmission_dta_result', ['assignmentid' => $assignmentid]); + $DB->delete_records('assignsubmission_dta_summary', ['assignmentid' => $assignmentid]); + } + + /** + * A call to this method should delete user data (where practical) using the userid and submission. + * + * @param assign_plugin_request_data $deletedata Details about the user and context to focus the deletion. + */ + public static function delete_submission_for_userid(assign_plugin_request_data $deletedata) { + global $DB; + + \core_plagiarism\privacy\provider::delete_plagiarism_for_user($deletedata->get_user()->id, $deletedata->get_context()); + + $assignmentid = $deletedata->get_assign()->get_instance()->id; + $submissionid = $deletedata->get_pluginobject()->id; + + $fs = get_file_storage(); + $fs->delete_area_files($deletedata->get_context()->id, 'assignsubmission_dta', ASSIGNSUBMISSION_DTA_FILEAREA_SUBMISSION, + $submissionid); + + // Delete records from assignsubmission_dta tables. Also possible with a list as below. + $DB->delete_records('assignsubmission_dta_result', [ + 'assignmentid' => $assignmentid, + 'submissionid' => $submissionid, + ]); + $DB->delete_records('assignsubmission_dta_summary', [ + 'assignmentid' => $assignmentid, + 'submissionid' => $submissionid, + ]); + } + + /** + * Deletes all submissions for the submission ids / userids provided in a context. + * assign_plugin_request_data contains: + * - context + * - assign object + * - submission ids (pluginids) + * - user ids + * @param assign_plugin_request_data $deletedata A class that contains the relevant information required for deletion. + */ + public static function delete_submissions(assign_plugin_request_data $deletedata) { + global $DB; + + \core_plagiarism\privacy\provider::delete_plagiarism_for_users($deletedata->get_userids(), $deletedata->get_context()); + + if (empty($deletedata->get_submissionids())) { + return; + } + $fs = get_file_storage(); + list($sql, $params) = $DB->get_in_or_equal($deletedata->get_submissionids(), SQL_PARAMS_NAMED); + $fs->delete_area_files_select($deletedata->get_context()->id, 'assignsubmission_file', ASSIGNSUBMISSION_FILE_FILEAREA, + $sql, $params); + + $params['assignid'] = $deletedata->get_assignid(); + $DB->delete_records_select('assignsubmission_dta_result', "assignmentid = :assignid AND submissionid $sql", $params); + $DB->delete_records_select('assignsubmission_dta_summary', "assignmentid = :assignid AND submissionid $sql", $params); + } + + /** + * Produce a list of files suitable for export that represent this feedback or submission + * + * @param stdClass $submission The submission + * @param stdClass $user The user record - unused + * @return array - return an array of files indexed by filename + */ + public function get_files(stdClass $submission, stdClass $user) { + $result = array(); + $fs = get_file_storage(); + + $files = $fs->get_area_files($this->assignment->get_context()->id, + 'assignsubmission_file', + ASSIGNSUBMISSION_DTA_FILEAREA_SUBMISSION, + $submission->id, + 'timemodified', + false); + + foreach ($files as $file) { + // Do we return the full folder path or just the file name? + if (isset($submission->exportfullpath) && $submission->exportfullpath == false) { + $result[$file->get_filename()] = $file; + } else { + $result[$file->get_filepath().$file->get_filename()] = $file; + } + } + return $result; + } + + +} \ No newline at end of file diff --git a/dta/utils/view.php b/dta/utils/view.php index 34d0881ca2a56ac8681547a173f3a72cc1b2bd88..ebd223a3c6b4df92cfee2b9f57a6ca956fc2e0e2 100644 --- a/dta/utils/view.php +++ b/dta/utils/view.php @@ -241,7 +241,7 @@ class view_submission_utils { // New copy of base attributes array. $resultrowattributes = $tablerowattributes; $tmp = ""; - $tmp .= html_writer::tag("td", get_string("comp" . $index, self::COMPONENT_NAME), $resultrowattributes); + $tmp .= html_writer::tag("td", get_string("comp" . $index, self::COMPONENT_NAME), $resultrowattributes); $tmp .= html_writer::tag("td", 100 * floatval($shown) / floatval($comp) . "% " . "(" . $shown . " / " . $comp . ")", $resultrowattributes); $tmp .= html_writer::tag("td", get_string("comp_expl" . $index, self::COMPONENT_NAME), $resultrowattributes); diff --git a/test/privacy/apicompliance.php b/test/privacy/apicompliance.php new file mode 100644 index 0000000000000000000000000000000000000000..121ad630786845a1575b9cb058284d6dd6279770 --- /dev/null +++ b/test/privacy/apicompliance.php @@ -0,0 +1,99 @@ +<?php + +// Set this if you want to run the script for one component only. Otherwise leave empty. +$CHECK_COMPONENT = ''; + +define('CLI_SCRIPT', true); + +require_once('config.php'); + +$user = \core_user::get_user(2); + +\core\session\manager::init_empty_session(); +\core\session\manager::set_user($user); + +$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 ($CHECK_COMPONENT && $component !== $CHECK_COMPONENT) { + continue; + } + $compliant = $manager->component_is_compliant($component); + if ($compliant) { + $list->good[] = $component; + } else { + $list->bad[] = $component; + } +} + +echo "The following plugins are not compliant:\n"; +echo "=> " . implode("\n=> ", array_values($list->bad)) . "\n"; + +echo "\n"; +echo "Testing the compliant plugins:\n"; +foreach ($list->good as $component) { + $classname = \core_privacy\manager::get_provider_classname_for_component($component); + echo "== {$component} ($classname) ==\n"; + if (check_implements($component, \core_privacy\local\metadata\null_provider::class)) { + echo " Claims not to store any data with reason:\n"; + echo " '" . 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); + $count = count($collection->get_collection()); + echo " Found {$count} items of metadata\n"; + if (empty($count)) { + echo "!!! 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; + echo " ".$item->get_name()." : ".get_string($item->get_summary(), $component) . "\n"; + } + } + if (!$userprefdescribed) { + echo "!!! 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); + echo " Successfully ran a test export\n"; + } else { + echo " Nothing to export.\n"; + } + } + if (check_implements($component, \core_privacy\local\request\shared_data_provider::class)) { + echo " This is a shared data provider\n"; + } + } +} + +echo "\n\n== Done ==\n"; + +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); +} \ No newline at end of file diff --git a/test/privacy/deletion.php b/test/privacy/deletion.php new file mode 100644 index 0000000000000000000000000000000000000000..8fdb75ce0023ad73b692d4cae61f6dbdb40a58ba --- /dev/null +++ b/test/privacy/deletion.php @@ -0,0 +1,70 @@ +<?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/>. + +define('CLI_SCRIPT', true); +require_once('config.php'); +require_once("$CFG->libdir/clilib.php"); + +list($options, $unrecognized) = cli_get_params( + [ + 'username' => '', + 'userid' => '', + ], + [] +); + +$user = null; +$username = $options['username']; +$userid = $options['userid']; + +if (!empty($options['username'])) { + $user = \core_user::get_user_by_username($options['username']); +} else if (!empty($options['userid'])) { + $user = \core_user::get_user($options['userid']); +} + +while (empty($user)) { + if (!empty($username)) { + echo "Unable to find a user with username '{$username}'.\n"; + echo "Try again.\n"; + } else if (!empty($userid)) { + echo "Unable to find a user with userid '{$userid}'.\n"; + echo "Try again.\n"; + } + $username = readline("Username: "); + $user = \core_user::get_user_by_username($username); +} + +echo "Processing delete for " . fullname($user) . "\n"; + +\core\session\manager::init_empty_session(); +\core\session\manager::set_user($user); + +$manager = new \core_privacy\manager(); + +$approvedlist = new \core_privacy\local\request\contextlist_collection($user->id); + +$trace = new text_progress_trace(); +$contextlists = $manager->get_contexts_for_userid($user->id, $trace); +foreach ($contextlists as $contextlist) { + $approvedlist->add_contextlist(new \core_privacy\local\request\approved_contextlist( + $user, + $contextlist->get_component(), + $contextlist->get_contextids() + )); +} + +$manager->delete_data_for_user($approvedlist, $trace); \ No newline at end of file diff --git a/test/privacy/export.php b/test/privacy/export.php new file mode 100644 index 0000000000000000000000000000000000000000..f56d61711f9fac857730470774ac5551a29d5ecd --- /dev/null +++ b/test/privacy/export.php @@ -0,0 +1,87 @@ +<?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/>. + +/** + * Helper utility to perform a test export. + * + * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk> + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define('CLI_SCRIPT', true); +require_once('config.php'); +require_once("$CFG->libdir/clilib.php"); + +list($options, $unrecognized) = cli_get_params( + [ + 'username' => '', + 'userid' => '', + ], + [] +); + +$user = null; +$username = $options['username']; +$userid = $options['userid']; + +if (!empty($options['username'])) { + $user = \core_user::get_user_by_username($options['username']); +} else if (!empty($options['userid'])) { + $user = \core_user::get_user($options['userid']); +} + +while (empty($user)) { + if (!empty($username)) { + echo "Unable to find a user with username '{$username}'.\n"; + echo "Try again.\n"; + } else if (!empty($userid)) { + echo "Unable to find a user with userid '{$userid}'.\n"; + echo "Try again.\n"; + } + $username = readline("Username: "); + $user = \core_user::get_user_by_username($username); +} + +echo "Processing export for " . fullname($user) . "\n"; + +\core\session\manager::init_empty_session(); +\core\session\manager::set_user($user); + +$PAGE = new moodle_page(); +$OUTPUT = new core_renderer($PAGE, RENDERER_TARGET_GENERAL); + +$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() + )); +} + +$exportedcontent = $manager->export_user_data($approvedlist); +$basedir = make_temp_directory('privacy'); +$exportpath = make_unique_writable_directory($basedir, true); +$fp = get_file_packer(); +$fp->extract_to_pathname($exportedcontent, $exportpath); + +echo "\n"; +echo "== File export was uncompressed to {$exportpath}\n"; +echo "============================================================================\n"; \ No newline at end of file