���ѧۧݧ�ӧ�� �ާ֧ߧ֧էا֧� - ���֧էѧܧ�ڧ��ӧѧ�� - /home3/cpr76684/public_html/dataprivacy.tar
���ѧ٧ѧ�
styles.css 0000644 00000001056 15152701722 0006605 0 ustar 00 .nav-pills .nav-pills { margin-left: 1rem; } .data-registry > .top-nav > * { margin-right: 0.5rem; } /*Extra attribute selection to have preference over bs2's .moodle-actionmenu[data-enhance] */ .data-registry > .top-nav > .singlebutton, .data-registry > .top-nav > .moodle-actionmenu[data-owner='dataregistry-actions'] { display: inline-block; } .data-registry .context-tree { height: 70vh; overflow-y: scroll; } [data-region="data-requests-table"] .moodle-actionmenu { min-width: 150px; } .context-level-view { margin: 1em; } createdatarequest.php 0000644 00000012701 15152701722 0010766 0 ustar 00 <?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/>. /** * Prints the contact form to the site's Data Protection Officer * * @copyright 2018 onwards Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU Public License * @package tool_dataprivacy */ require_once('../../../config.php'); require_once('lib.php'); require_once('createdatarequest_form.php'); $manage = optional_param('manage', 0, PARAM_INT); $requesttype = optional_param('type', \tool_dataprivacy\api::DATAREQUEST_TYPE_EXPORT, PARAM_INT); $url = new moodle_url('/admin/tool/dataprivacy/createdatarequest.php', ['manage' => $manage, 'type' => $requesttype]); $PAGE->set_url($url); require_login(); if (isguestuser()) { throw new \moodle_exception('noguest'); } // Return URL and context. if ($manage) { // For the case where DPO creates data requests on behalf of another user. $returnurl = new moodle_url($CFG->wwwroot . '/admin/tool/dataprivacy/datarequests.php'); $context = context_system::instance(); // Make sure the user has the proper capability. require_capability('tool/dataprivacy:managedatarequests', $context); navigation_node::override_active_url($returnurl); } else { // For the case where a user makes request for themselves (or for their children if they are the parent). $returnurl = new moodle_url($CFG->wwwroot . '/admin/tool/dataprivacy/mydatarequests.php'); $context = context_user::instance($USER->id); } $PAGE->set_context($context); if (!$manage && $profilenode = $PAGE->settingsnav->find('myprofile', null)) { $profilenode->make_active(); } $title = get_string('createnewdatarequest', 'tool_dataprivacy'); $PAGE->navbar->add($title); // If contactdataprotectionofficer is disabled, send the user back to the profile page, or the privacy policy page. // That is, unless you have sufficient capabilities to perform this on behalf of a user. if (!$manage && !\tool_dataprivacy\api::can_contact_dpo()) { redirect($returnurl, get_string('contactdpoviaprivacypolicy', 'tool_dataprivacy'), 0, \core\output\notification::NOTIFY_ERROR); } $mform = new tool_dataprivacy_data_request_form($url->out(false), ['manage' => !empty($manage), 'persistent' => new \tool_dataprivacy\data_request(0, (object) ['type' => $requesttype])]); // Data request cancelled. if ($mform->is_cancelled()) { redirect($returnurl); } // Data request submitted. if ($data = $mform->get_data()) { if ($data->userid != $USER->id) { if (!\tool_dataprivacy\api::can_manage_data_requests($USER->id)) { // If not a DPO, only users with the capability to make data requests for the user should be allowed. // (e.g. users with the Parent role, etc). \tool_dataprivacy\api::require_can_create_data_request_for_user($data->userid); } } if ($data->type == \tool_dataprivacy\api::DATAREQUEST_TYPE_DELETE) { if ($data->userid == $USER->id) { if (!\tool_dataprivacy\api::can_create_data_deletion_request_for_self()) { throw new moodle_exception('nopermissions', 'error', '', get_string('errorcannotrequestdeleteforself', 'tool_dataprivacy')); } } else if (!\tool_dataprivacy\api::can_create_data_deletion_request_for_other() && !\tool_dataprivacy\api::can_create_data_deletion_request_for_children($data->userid)) { throw new moodle_exception('nopermissions', 'error', '', get_string('errorcannotrequestdeleteforother', 'tool_dataprivacy')); } } else if ($data->type == \tool_dataprivacy\api::DATAREQUEST_TYPE_EXPORT) { if ($data->userid == $USER->id && !\tool_dataprivacy\api::can_create_data_download_request_for_self()) { throw new moodle_exception('nopermissions', 'error', '', get_string('errorcannotrequestexportforself', 'tool_dataprivacy')); } } \tool_dataprivacy\api::create_data_request($data->userid, $data->type, $data->comments); if ($manage) { $foruser = core_user::get_user($data->userid); $redirectmessage = get_string('datarequestcreatedforuser', 'tool_dataprivacy', fullname($foruser)); } else if (\tool_dataprivacy\api::is_automatic_request_approval_on($data->type)) { // Let the user know that the request has been submitted and will be processed soon. $redirectmessage = get_string('approvedrequestsubmitted', 'tool_dataprivacy'); } else { // Let the user know that the request has been submitted to the privacy officer. $redirectmessage = get_string('requestsubmitted', 'tool_dataprivacy'); } redirect($returnurl, $redirectmessage); } $PAGE->set_heading($SITE->fullname); $PAGE->set_title($title); echo $OUTPUT->header(); echo $OUTPUT->heading($title); echo $OUTPUT->box_start('createrequestform'); $mform->display(); echo $OUTPUT->box_end(); echo $OUTPUT->footer(); summary.php 0000644 00000003265 15152701722 0006762 0 ustar 00 <?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/>. /** * Prints the compliance data registry main page. * * @copyright 2018 onwards Adrian Greeve <adriangreeve.com> * @license http://www.gnu.org/copyleft/gpl.html GNU Public License * @package tool_dataprivacy */ require_once(__DIR__ . '/../../../config.php'); require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php'); $url = new moodle_url('/' . $CFG->admin . '/tool/dataprivacy/summary.php'); $title = get_string('summary', 'tool_dataprivacy'); $context = \context_system::instance(); $PAGE->set_url($url); $PAGE->set_context($context); $PAGE->set_title($title); $PAGE->set_heading($SITE->fullname); // If user is logged in, then use profile navigation in breadcrumbs. if ($profilenode = $PAGE->settingsnav->find('myprofile', null)) { $profilenode->make_active(); } $PAGE->navbar->add($title); $output = $PAGE->get_renderer('tool_dataprivacy'); echo $output->header(); $summarypage = new \tool_dataprivacy\output\summary_page(); echo $output->render($summarypage); echo $output->footer(); purposes.php 0000644 00000002627 15152701722 0007146 0 ustar 00 <?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/>. /** * This page lets users manage purposes. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once(__DIR__ . '/../../../config.php'); require_login(null, false); $url = new moodle_url("/admin/tool/dataprivacy/purposes.php"); $title = get_string('editpurposes', 'tool_dataprivacy'); \tool_dataprivacy\page_helper::setup($url, $title, 'dataregistry'); $output = $PAGE->get_renderer('tool_dataprivacy'); echo $output->header(); echo $output->heading($title); $purposes = \tool_dataprivacy\api::get_purposes(); $renderable = new \tool_dataprivacy\output\purposes($purposes); echo $output->render($renderable); echo $output->footer(); db/caches.php 0000644 00000003141 15152701722 0007071 0 ustar 00 <?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/>. /** * tool_dataprivacy cache definitions. * * @package tool_dataprivacy * @category cache * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $definitions = array( 'purpose' => array( 'mode' => cache_store::MODE_APPLICATION, 'simplekeys' => true, 'simpledata' => true, 'staticacceleration' => true, 'staticaccelerationsize' => 30, ), 'purpose_overrides' => array( 'mode' => cache_store::MODE_APPLICATION, 'simplekeys' => true, 'simpledata' => false, 'staticacceleration' => true, 'staticaccelerationsize' => 50, ), 'contextlevel' => array( 'mode' => cache_store::MODE_APPLICATION, 'simplekeys' => true, 'simpledata' => true, 'staticacceleration' => true, 'staticaccelerationsize' => 10, ), ); db/upgrade.php 0000644 00000006447 15152701722 0007306 0 ustar 00 <?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/>. /** * tool_dataprivacy plugin upgrade code * * @package tool_dataprivacy * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Function to upgrade tool_dataprivacy. * * @param int $oldversion the version we are upgrading from * @return bool result */ function xmldb_tool_dataprivacy_upgrade($oldversion) { global $CFG, $DB; $dbman = $DB->get_manager(); // Automatically generated Moodle v3.9.0 release upgrade line. // Put any upgrade step following this. if ($oldversion < 2020061501) { // Define field commentsformat to be added to tool_dataprivacy_request. $table = new xmldb_table('tool_dataprivacy_request'); $field = new xmldb_field('commentsformat', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0', 'comments'); // Conditionally launch add field commentsformat. if (!$dbman->field_exists($table, $field)) { $dbman->add_field($table, $field); } // Define field dpocommentformat to be added to tool_dataprivacy_request. $field = new xmldb_field('dpocommentformat', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0', 'dpocomment'); // Conditionally launch add field dpocommentformat. if (!$dbman->field_exists($table, $field)) { $dbman->add_field($table, $field); } // Define field systemapproved to be added to tool_dataprivacy_request. $field = new xmldb_field('systemapproved', XMLDB_TYPE_INTEGER, '4', null, XMLDB_NOTNULL, null, '0', 'dpocommentformat'); // Conditionally launch add field systemapproved. if (!$dbman->field_exists($table, $field)) { $dbman->add_field($table, $field); } // Dataprivacy savepoint reached. upgrade_plugin_savepoint(true, 2020061501, 'tool', 'dataprivacy'); } // Automatically generated Moodle v4.0.0 release upgrade line. // Put any upgrade step following this. if ($oldversion < 2022053000) { // Define key usermodified (foreign) to be added to tool_dataprivacy_purposerole. $table = new xmldb_table('tool_dataprivacy_purposerole'); $key = new xmldb_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']); // Launch add key usermodified. $dbman->add_key($table, $key); // Dataprivacy savepoint reached. upgrade_plugin_savepoint(true, 2022053000, 'tool', 'dataprivacy'); } // Automatically generated Moodle v4.1.0 release upgrade line. // Put any upgrade step following this. return true; } db/tasks.php 0000644 00000003661 15152701722 0006777 0 ustar 00 <?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/>. /** * This file defines tasks performed by the tool. * * @package tool_dataprivacy * @copyright 2018 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); // List of tasks. $tasks = array( array( 'classname' => 'tool_dataprivacy\task\expired_retention_period', 'blocking' => 0, 'minute' => '0', 'hour' => 'R', 'day' => '*', 'dayofweek' => '*', 'month' => '*' ), array( 'classname' => 'tool_dataprivacy\task\delete_expired_contexts', 'blocking' => 0, 'minute' => '0', 'hour' => 'R', 'day' => '*', 'dayofweek' => '*', 'month' => '*' ), array( 'classname' => 'tool_dataprivacy\task\delete_expired_requests', 'blocking' => 0, 'minute' => 'R', 'hour' => 'R', 'day' => '*', 'dayofweek' => '*', 'month' => '*' ), array( 'classname' => 'tool_dataprivacy\task\delete_existing_deleted_users', 'blocking' => 0, 'minute' => 'R', 'hour' => 'R', 'day' => '*', 'dayofweek' => '*', 'month' => '*', 'disabled' => true, ), ); db/services.php 0000644 00000022602 15152701722 0007471 0 ustar 00 <?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/>. /** * Chat external functions and service definitions. * * @package tool_dataprivacy * @category external * @copyright 2018 Jun Pataleta <jun@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; $functions = [ 'tool_dataprivacy_cancel_data_request' => [ 'classname' => 'tool_dataprivacy\external', 'methodname' => 'cancel_data_request', 'classpath' => '', 'description' => 'Cancel the data request made by the user', 'type' => 'write', 'capabilities' => '', 'ajax' => true, 'loginrequired' => true, ], 'tool_dataprivacy_contact_dpo' => [ 'classname' => 'tool_dataprivacy\external', 'methodname' => 'contact_dpo', 'classpath' => '', 'description' => 'Contact the site Data Protection Officer(s)', 'type' => 'write', 'capabilities' => '', 'ajax' => true, 'loginrequired' => true, ], 'tool_dataprivacy_mark_complete' => [ 'classname' => 'tool_dataprivacy\external', 'methodname' => 'mark_complete', 'classpath' => '', 'description' => 'Mark a user\'s general enquiry as complete', 'type' => 'write', 'capabilities' => 'tool/dataprivacy:managedatarequests', 'ajax' => true, 'loginrequired' => true, ], 'tool_dataprivacy_get_data_request' => [ 'classname' => 'tool_dataprivacy\external', 'methodname' => 'get_data_request', 'classpath' => '', 'description' => 'Fetch the details of a user\'s data request', 'type' => 'read', 'capabilities' => 'tool/dataprivacy:managedatarequests', 'ajax' => true, 'loginrequired' => true, ], 'tool_dataprivacy_approve_data_request' => [ 'classname' => 'tool_dataprivacy\external', 'methodname' => 'approve_data_request', 'classpath' => '', 'description' => 'Approve a data request', 'type' => 'write', 'capabilities' => 'tool/dataprivacy:managedatarequests', 'ajax' => true, 'loginrequired' => true, ], 'tool_dataprivacy_bulk_approve_data_requests' => [ 'classname' => 'tool_dataprivacy\external', 'methodname' => 'bulk_approve_data_requests', 'classpath' => '', 'description' => 'Bulk approve data requests', 'type' => 'write', 'capabilities' => 'tool/dataprivacy:managedatarequests', 'ajax' => true, 'loginrequired' => true, ], 'tool_dataprivacy_deny_data_request' => [ 'classname' => 'tool_dataprivacy\external', 'methodname' => 'deny_data_request', 'classpath' => '', 'description' => 'Deny a data request', 'type' => 'write', 'capabilities' => 'tool/dataprivacy:managedatarequests', 'ajax' => true, 'loginrequired' => true, ], 'tool_dataprivacy_bulk_deny_data_requests' => [ 'classname' => 'tool_dataprivacy\external', 'methodname' => 'bulk_deny_data_requests', 'classpath' => '', 'description' => 'Bulk deny data requests', 'type' => 'write', 'capabilities' => 'tool/dataprivacy:managedatarequests', 'ajax' => true, 'loginrequired' => true, ], 'tool_dataprivacy_get_users' => [ 'classname' => 'tool_dataprivacy\external', 'methodname' => 'get_users', 'classpath' => '', 'description' => 'Fetches a list of users', 'type' => 'read', 'capabilities' => 'tool/dataprivacy:managedatarequests', 'ajax' => true, 'loginrequired' => true, ], 'tool_dataprivacy_create_purpose_form' => [ 'classname' => 'tool_dataprivacy\external', 'methodname' => 'create_purpose_form', 'classpath' => '', 'description' => 'Adds a data purpose', 'type' => 'write', 'capabilities' => '', 'ajax' => true, 'loginrequired' => true, ], 'tool_dataprivacy_create_category_form' => [ 'classname' => 'tool_dataprivacy\external', 'methodname' => 'create_category_form', 'classpath' => '', 'description' => 'Adds a data category', 'type' => 'write', 'capabilities' => '', 'ajax' => true, 'loginrequired' => true, ], 'tool_dataprivacy_delete_purpose' => [ 'classname' => 'tool_dataprivacy\external', 'methodname' => 'delete_purpose', 'classpath' => '', 'description' => 'Deletes an existing data purpose', 'type' => 'write', 'capabilities' => '', 'ajax' => true, 'loginrequired' => true, ], 'tool_dataprivacy_delete_category' => [ 'classname' => 'tool_dataprivacy\external', 'methodname' => 'delete_category', 'classpath' => '', 'description' => 'Deletes an existing data category', 'type' => 'write', 'capabilities' => '', 'ajax' => true, 'loginrequired' => true, ], 'tool_dataprivacy_set_contextlevel_form' => [ 'classname' => 'tool_dataprivacy\external', 'methodname' => 'set_contextlevel_form', 'classpath' => '', 'description' => 'Sets purpose and category across a context level', 'type' => 'write', 'capabilities' => '', 'ajax' => true, 'loginrequired' => true, ], 'tool_dataprivacy_set_context_form' => [ 'classname' => 'tool_dataprivacy\external', 'methodname' => 'set_context_form', 'classpath' => '', 'description' => 'Sets purpose and category for a specific context', 'type' => 'write', 'capabilities' => '', 'ajax' => true, 'loginrequired' => true, ], 'tool_dataprivacy_tree_extra_branches' => [ 'classname' => 'tool_dataprivacy\external', 'methodname' => 'tree_extra_branches', 'classpath' => '', 'description' => 'Return branches for the context tree', 'type' => 'write', 'capabilities' => '', 'ajax' => true, 'loginrequired' => true, ], 'tool_dataprivacy_confirm_contexts_for_deletion' => [ 'classname' => 'tool_dataprivacy\external', 'methodname' => 'confirm_contexts_for_deletion', 'classpath' => '', 'description' => 'Mark the selected expired contexts as confirmed for deletion', 'type' => 'write', 'capabilities' => '', 'ajax' => true, 'loginrequired' => true, ], 'tool_dataprivacy_set_context_defaults' => [ 'classname' => 'tool_dataprivacy\external', 'methodname' => 'set_context_defaults', 'classpath' => '', 'description' => 'Updates the default category and purpose for a given context level (and optionally, a plugin)', 'type' => 'write', 'capabilities' => 'tool/dataprivacy:managedataregistry', 'ajax' => true, 'loginrequired' => true, ], 'tool_dataprivacy_get_category_options' => [ 'classname' => 'tool_dataprivacy\external', 'methodname' => 'get_category_options', 'classpath' => '', 'description' => 'Fetches a list of data category options', 'type' => 'read', 'capabilities' => 'tool/dataprivacy:managedataregistry', 'ajax' => true, 'loginrequired' => true, ], 'tool_dataprivacy_get_purpose_options' => [ 'classname' => 'tool_dataprivacy\external', 'methodname' => 'get_purpose_options', 'classpath' => '', 'description' => 'Fetches a list of data storage purpose options', 'type' => 'read', 'capabilities' => 'tool/dataprivacy:managedataregistry', 'ajax' => true, 'loginrequired' => true, ], 'tool_dataprivacy_get_activity_options' => [ 'classname' => 'tool_dataprivacy\external', 'methodname' => 'get_activity_options', 'classpath' => '', 'description' => 'Fetches a list of activity options', 'type' => 'read', 'capabilities' => 'tool/dataprivacy:managedataregistry', 'ajax' => true, 'loginrequired' => true, ], ]; db/events.php 0000644 00000002164 15152701722 0007153 0 ustar 00 <?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/>. /** * This file defines observers needed by the plugin. * * @package tool_dataprivacy * @copyright 2018 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $observers = [ [ 'eventname' => '\core\event\user_deleted', 'callback' => '\tool_dataprivacy\event\user_deleted_observer::create_delete_data_request', ], ]; db/messages.php 0000644 00000003510 15152701722 0007452 0 ustar 00 <?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/>. /** * Defines message providers (types of messages being sent) * * @package tool_dataprivacy * @copyright 2018 onwards Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $messageproviders = [ // Notify Data Protection Officer about incoming data requests. 'contactdataprotectionofficer' => [ 'defaults' => [ 'popup' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_ENABLED, 'email' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_ENABLED, ], 'capability' => 'tool/dataprivacy:managedatarequests' ], // Notify user about the processing results of their data request. 'datarequestprocessingresults' => [ 'defaults' => [ 'popup' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_ENABLED, 'email' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_ENABLED, ] ], // Notify Data Protection Officer about exceptions. 'notifyexceptions' => [ 'defaults' => [ 'email' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_ENABLED, ], 'capability' => 'tool/dataprivacy:managedatarequests' ], ]; db/access.php 0000644 00000007024 15152701722 0007110 0 ustar 00 <?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/>. /** * Capability definitions for this module. * * @package tool_dataprivacy * @copyright 2018 onwards Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $capabilities = [ // Capability for managing data requests. Usually given to the site's Data Protection Officer. 'tool/dataprivacy:managedatarequests' => [ 'riskbitmask' => RISK_SPAM | RISK_PERSONAL | RISK_XSS | RISK_DATALOSS, 'captype' => 'write', 'contextlevel' => CONTEXT_SYSTEM, 'archetypes' => [] ], // Capability for create new delete data request. Usually given to the site's Protection Officer. 'tool/dataprivacy:requestdeleteforotheruser' => [ 'riskbitmask' => RISK_SPAM | RISK_PERSONAL | RISK_XSS | RISK_DATALOSS, 'captype' => 'write', 'contextlevel' => CONTEXT_SYSTEM, 'archetypes' => [], 'clonepermissionsfrom' => 'tool/dataprivacy:managedatarequests' ], // Capability for managing the data registry. Usually given to the site's Data Protection Officer. 'tool/dataprivacy:managedataregistry' => [ 'riskbitmask' => RISK_SPAM | RISK_PERSONAL | RISK_XSS | RISK_DATALOSS, 'captype' => 'write', 'contextlevel' => CONTEXT_SYSTEM, 'archetypes' => [] ], // Capability for parents/guardians to make data requests on behalf of their children. 'tool/dataprivacy:makedatarequestsforchildren' => [ 'riskbitmask' => RISK_SPAM | RISK_PERSONAL, 'captype' => 'write', 'contextlevel' => CONTEXT_USER, 'archetypes' => [] ], // Capability for parents/guardians to make delete data requests on behalf of their children. 'tool/dataprivacy:makedatadeletionrequestsforchildren' => [ 'riskbitmask' => RISK_SPAM | RISK_PERSONAL, 'captype' => 'write', 'contextlevel' => CONTEXT_USER, 'archetypes' => [], 'clonepermissionsfrom' => 'tool/dataprivacy:makedatarequestsforchildren' ], // Capability for users to download the results of their own data request. 'tool/dataprivacy:downloadownrequest' => [ 'riskbitmask' => 0, 'captype' => 'read', 'contextlevel' => CONTEXT_USER, 'archetypes' => [ 'user' => CAP_ALLOW ] ], // Capability for administrators to download other people's data requests. 'tool/dataprivacy:downloadallrequests' => [ 'riskbitmask' => RISK_PERSONAL, 'captype' => 'read', 'contextlevel' => CONTEXT_USER, 'archetypes' => [] ], // Capability for users to create delete data request for their own. 'tool/dataprivacy:requestdelete' => [ 'riskbitmask' => RISK_DATALOSS, 'captype' => 'write', 'contextlevel' => CONTEXT_USER, 'archetypes' => [ 'user' => CAP_ALLOW ] ] ]; db/install.xml 0000644 00000024076 15152701722 0007334 0 ustar 00 <?xml version="1.0" encoding="UTF-8" ?> <XMLDB PATH="admin/tool/dataprivacy/db" VERSION="20220530" COMMENT="XMLDB file for Moodle tool/dataprivacy" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../lib/xmldb/xmldb.xsd" > <TABLES> <TABLE NAME="tool_dataprivacy_request" COMMENT="Table for data requests"> <FIELDS> <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/> <FIELD NAME="type" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Data request type"/> <FIELD NAME="comments" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="More details about the request"/> <FIELD NAME="commentsformat" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> <FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The user ID the request is being made for"/> <FIELD NAME="requestedby" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The user ID of the one making the request"/> <FIELD NAME="status" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The current status of the data request"/> <FIELD NAME="dpo" TYPE="int" LENGTH="10" NOTNULL="false" DEFAULT="0" SEQUENCE="false" COMMENT="The user ID of the Data Protection Officer who is reviewing th request"/> <FIELD NAME="dpocomment" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="DPO's comments (e.g. reason for rejecting the request, etc.)"/> <FIELD NAME="dpocommentformat" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> <FIELD NAME="systemapproved" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> <FIELD NAME="usermodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The user who created/modified this request object"/> <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The time this data request was created"/> <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The last time this data request was updated"/> <FIELD NAME="creationmethod" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The type of the creation method of the data request"/> </FIELDS> <KEYS> <KEY NAME="primary" TYPE="primary" FIELDS="id"/> <KEY NAME="userid" TYPE="foreign" FIELDS="userid" REFTABLE="user" REFFIELDS="id"/> <KEY NAME="requestedby" TYPE="foreign" FIELDS="requestedby" REFTABLE="user" REFFIELDS="id"/> <KEY NAME="dpo" TYPE="foreign" FIELDS="dpo" REFTABLE="user" REFFIELDS="id"/> <KEY NAME="usermodified" TYPE="foreign" FIELDS="usermodified" REFTABLE="user" REFFIELDS="id"/> </KEYS> </TABLE> <TABLE NAME="tool_dataprivacy_purpose" COMMENT="Data purposes"> <FIELDS> <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/> <FIELD NAME="name" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false"/> <FIELD NAME="description" TYPE="text" NOTNULL="false" SEQUENCE="false"/> <FIELD NAME="descriptionformat" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false"/> <FIELD NAME="lawfulbases" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="Comma-separated IDs matching records in tool_dataprivacy_lawfulbasis"/> <FIELD NAME="sensitivedatareasons" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Comma-separated IDs matching records in tool_dataprivacy_sensitive"/> <FIELD NAME="retentionperiod" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/> <FIELD NAME="protected" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false"/> <FIELD NAME="usermodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/> <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/> <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/> </FIELDS> <KEYS> <KEY NAME="primary" TYPE="primary" FIELDS="id"/> </KEYS> </TABLE> <TABLE NAME="tool_dataprivacy_category" COMMENT="Data categories"> <FIELDS> <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/> <FIELD NAME="name" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false"/> <FIELD NAME="description" TYPE="text" NOTNULL="false" SEQUENCE="false"/> <FIELD NAME="descriptionformat" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false"/> <FIELD NAME="usermodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/> <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/> <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/> </FIELDS> <KEYS> <KEY NAME="primary" TYPE="primary" FIELDS="id"/> </KEYS> </TABLE> <TABLE NAME="tool_dataprivacy_ctxinstance" COMMENT="Default comment for the table, please edit me"> <FIELDS> <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/> <FIELD NAME="contextid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/> <FIELD NAME="purposeid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/> <FIELD NAME="categoryid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/> <FIELD NAME="usermodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/> <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/> <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/> </FIELDS> <KEYS> <KEY NAME="primary" TYPE="primary" FIELDS="id"/> <KEY NAME="contextid" TYPE="foreign-unique" FIELDS="contextid" REFTABLE="context" REFFIELDS="id"/> <KEY NAME="purposeid" TYPE="foreign" FIELDS="purposeid" REFTABLE="tool_dataprivacy_purpose" REFFIELDS="id"/> <KEY NAME="categoryid" TYPE="foreign" FIELDS="categoryid" REFTABLE="tool_dataprivacy_category" REFFIELDS="id"/> </KEYS> </TABLE> <TABLE NAME="tool_dataprivacy_ctxlevel" COMMENT="Default comment for the table, please edit me"> <FIELDS> <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/> <FIELD NAME="contextlevel" TYPE="int" LENGTH="3" NOTNULL="true" SEQUENCE="false"/> <FIELD NAME="purposeid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/> <FIELD NAME="categoryid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/> <FIELD NAME="usermodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/> <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/> <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/> </FIELDS> <KEYS> <KEY NAME="primary" TYPE="primary" FIELDS="id"/> <KEY NAME="contextlevel" TYPE="unique" FIELDS="contextlevel"/> <KEY NAME="categoryid" TYPE="foreign" FIELDS="categoryid" REFTABLE="tool_dataprivacy_category" REFFIELDS="id"/> <KEY NAME="purposeid" TYPE="foreign" FIELDS="purposeid" REFTABLE="tool_dataprivacy_purpose" REFFIELDS="id"/> </KEYS> </TABLE> <TABLE NAME="tool_dataprivacy_ctxexpired" COMMENT="Default comment for the table, please edit me"> <FIELDS> <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/> <FIELD NAME="contextid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/> <FIELD NAME="unexpiredroles" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Roles which have explicitly not expired yet."/> <FIELD NAME="expiredroles" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Explicitly expires roles"/> <FIELD NAME="defaultexpired" TYPE="int" LENGTH="1" NOTNULL="true" SEQUENCE="false" COMMENT="The default retention period has passed."/> <FIELD NAME="status" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> <FIELD NAME="usermodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/> <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/> <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/> </FIELDS> <KEYS> <KEY NAME="primary" TYPE="primary" FIELDS="id"/> <KEY NAME="contextid" TYPE="foreign-unique" FIELDS="contextid" REFTABLE="context" REFFIELDS="id"/> </KEYS> </TABLE> <TABLE NAME="tool_dataprivacy_purposerole" COMMENT="Data purpose overrides for a specific role"> <FIELDS> <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/> <FIELD NAME="purposeid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/> <FIELD NAME="roleid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/> <FIELD NAME="lawfulbases" TYPE="text" NOTNULL="false" SEQUENCE="false"/> <FIELD NAME="sensitivedatareasons" TYPE="text" NOTNULL="false" SEQUENCE="false"/> <FIELD NAME="retentionperiod" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/> <FIELD NAME="protected" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false"/> <FIELD NAME="usermodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/> <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/> <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/> </FIELDS> <KEYS> <KEY NAME="primary" TYPE="primary" FIELDS="id"/> <KEY NAME="purposepurposeid" TYPE="foreign" FIELDS="purposeid" REFTABLE="tool_dataprivacy_purpose" REFFIELDS="id"/> <KEY NAME="puproseroleid" TYPE="foreign" FIELDS="roleid" REFTABLE="role" REFFIELDS="id"/> <KEY NAME="usermodified" TYPE="foreign" FIELDS="usermodified" REFTABLE="user" REFFIELDS="id"/> </KEYS> <INDEXES> <INDEX NAME="purposerole" UNIQUE="true" FIELDS="purposeid, roleid"/> </INDEXES> </TABLE> </TABLES> </XMLDB> categories.php 0000644 00000002645 15152701722 0007413 0 ustar 00 <?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/>. /** * This page lets users manage categories. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once(__DIR__ . '/../../../config.php'); require_login(null, false); $url = new moodle_url("/admin/tool/dataprivacy/categories.php"); $title = get_string('editcategories', 'tool_dataprivacy'); \tool_dataprivacy\page_helper::setup($url, $title, 'dataregistry'); $output = $PAGE->get_renderer('tool_dataprivacy'); echo $output->header(); echo $output->heading($title); $categories = \tool_dataprivacy\api::get_categories(); $renderable = new \tool_dataprivacy\output\categories($categories); echo $output->render($renderable); echo $output->footer(); resubmitrequest.php 0000644 00000004717 15152701722 0010533 0 ustar 00 <?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/>. /** * Display the request reject + resubmit confirmation page. * * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU Public License * @package tool_dataprivacy */ require_once('../../../config.php'); $requestid = required_param('requestid', PARAM_INT); $confirm = optional_param('confirm', null, PARAM_INT); $url = new moodle_url('/admin/tool/dataprivacy/resubmitrequest.php', ['requestid' => $requestid]); $title = get_string('resubmitrequestasnew', 'tool_dataprivacy'); \tool_dataprivacy\page_helper::setup($url, $title, 'datarequests', 'tool/dataprivacy:managedatarequests'); $manageurl = new moodle_url('/admin/tool/dataprivacy/datarequests.php'); $originalrequest = \tool_dataprivacy\api::get_request($requestid); $user = \core_user::get_user($originalrequest->get('userid')); $stringparams = (object) [ 'username' => fullname($user), 'type' => \tool_dataprivacy\local\helper::get_shortened_request_type_string($originalrequest->get('type')), ]; if (null !== $confirm && confirm_sesskey()) { if ($originalrequest->get('type') == \tool_dataprivacy\api::DATAREQUEST_TYPE_DELETE && !\tool_dataprivacy\api::can_create_data_deletion_request_for_other()) { throw new required_capability_exception(context_system::instance(), 'tool/dataprivacy:requestdeleteforotheruser', 'nopermissions', ''); } $originalrequest->resubmit_request(); redirect($manageurl, get_string('resubmittedrequest', 'tool_dataprivacy', $stringparams)); } echo $OUTPUT->header(); $confirmstring = get_string('confirmrequestresubmit', 'tool_dataprivacy', $stringparams); $confirmurl = new \moodle_url($PAGE->url, ['confirm' => 1]); echo $OUTPUT->confirm($confirmstring, $confirmurl, $manageurl); echo $OUTPUT->footer(); amd/build/categoriesactions.min.js.map 0000644 00000013500 15152701722 0014007 0 ustar 00 {"version":3,"file":"categoriesactions.min.js","sources":["../src/categoriesactions.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * AMD module for categories actions.\n *\n * @module tool_dataprivacy/categoriesactions\n * @copyright 2018 David Monllao\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([\n 'jquery',\n 'core/ajax',\n 'core/notification',\n 'core/str',\n 'core/modal_factory',\n 'core/modal_events'],\nfunction($, Ajax, Notification, Str, ModalFactory, ModalEvents) {\n\n /**\n * List of action selectors.\n *\n * @type {{DELETE: string}}\n */\n var ACTIONS = {\n DELETE: '[data-action=\"deletecategory\"]',\n };\n\n /**\n * CategoriesActions class.\n */\n var CategoriesActions = function() {\n this.registerEvents();\n };\n\n /**\n * Register event listeners.\n */\n CategoriesActions.prototype.registerEvents = function() {\n $(ACTIONS.DELETE).click(function(e) {\n e.preventDefault();\n\n var id = $(this).data('id');\n var categoryname = $(this).data('name');\n var stringkeys = [\n {\n key: 'deletecategory',\n component: 'tool_dataprivacy'\n },\n {\n key: 'deletecategorytext',\n component: 'tool_dataprivacy',\n param: categoryname\n },\n {\n key: 'delete'\n }\n ];\n\n Str.get_strings(stringkeys).then(function(langStrings) {\n var title = langStrings[0];\n var confirmMessage = langStrings[1];\n var buttonText = langStrings[2];\n return ModalFactory.create({\n title: title,\n body: confirmMessage,\n type: ModalFactory.types.SAVE_CANCEL\n }).then(function(modal) {\n modal.setSaveButtonText(buttonText);\n\n // Handle save event.\n modal.getRoot().on(ModalEvents.save, function() {\n\n var request = {\n methodname: 'tool_dataprivacy_delete_category',\n args: {'id': id}\n };\n\n Ajax.call([request])[0].done(function(data) {\n if (data.result) {\n $('tr[data-categoryid=\"' + id + '\"]').remove();\n } else {\n Notification.addNotification({\n message: data.warnings[0].message,\n type: 'error'\n });\n }\n }).fail(Notification.exception);\n });\n\n // Handle hidden event.\n modal.getRoot().on(ModalEvents.hidden, function() {\n // Destroy when hidden.\n modal.destroy();\n });\n\n return modal;\n });\n }).done(function(modal) {\n modal.show();\n\n }).fail(Notification.exception);\n });\n };\n\n return /** @alias module:tool_dataprivacy/categoriesactions */ {\n // Public variables and functions.\n\n /**\n * Initialise the module.\n *\n * @method init\n * @return {CategoriesActions}\n */\n 'init': function() {\n return new CategoriesActions();\n }\n };\n});\n"],"names":["define","$","Ajax","Notification","Str","ModalFactory","ModalEvents","ACTIONS","CategoriesActions","registerEvents","prototype","click","e","preventDefault","id","this","data","stringkeys","key","component","param","get_strings","then","langStrings","title","confirmMessage","buttonText","create","body","type","types","SAVE_CANCEL","modal","setSaveButtonText","getRoot","on","save","request","methodname","args","call","done","result","remove","addNotification","message","warnings","fail","exception","hidden","destroy","show"],"mappings":";;;;;;;AAsBAA,4CAAO,CACH,SACA,YACA,oBACA,WACA,qBACA,sBACJ,SAASC,EAAGC,KAAMC,aAAcC,IAAKC,aAAcC,iBAO3CC,eACQ,iCAMRC,kBAAoB,gBACfC,yBAMTD,kBAAkBE,UAAUD,eAAiB,WACzCR,EAAEM,gBAAgBI,OAAM,SAASC,GAC7BA,EAAEC,qBAEEC,GAAKb,EAAEc,MAAMC,KAAK,MAElBC,WAAa,CACb,CACIC,IAAK,iBACLC,UAAW,oBAEf,CACID,IAAK,qBACLC,UAAW,mBACXC,MATWnB,EAAEc,MAAMC,KAAK,SAW5B,CACIE,IAAK,WAIbd,IAAIiB,YAAYJ,YAAYK,MAAK,SAASC,iBAClCC,MAAQD,YAAY,GACpBE,eAAiBF,YAAY,GAC7BG,WAAaH,YAAY,UACtBlB,aAAasB,OAAO,CACvBH,MAAOA,MACPI,KAAMH,eACNI,KAAMxB,aAAayB,MAAMC,cAC1BT,MAAK,SAASU,cACbA,MAAMC,kBAAkBP,YAGxBM,MAAME,UAAUC,GAAG7B,YAAY8B,MAAM,eAE7BC,QAAU,CACVC,WAAY,mCACZC,KAAM,IAAOzB,KAGjBZ,KAAKsC,KAAK,CAACH,UAAU,GAAGI,MAAK,SAASzB,MAC9BA,KAAK0B,OACLzC,EAAE,uBAAyBa,GAAK,MAAM6B,SAEtCxC,aAAayC,gBAAgB,CACzBC,QAAS7B,KAAK8B,SAAS,GAAGD,QAC1BhB,KAAM,aAGfkB,KAAK5C,aAAa6C,cAIzBhB,MAAME,UAAUC,GAAG7B,YAAY2C,QAAQ,WAEnCjB,MAAMkB,aAGHlB,YAEZS,MAAK,SAAST,OACbA,MAAMmB,UAEPJ,KAAK5C,aAAa6C,eAIkC,MASnD,kBACG,IAAIxC"} amd/build/purposesactions.min.js 0000644 00000003223 15152701722 0012767 0 ustar 00 /** * AMD module for purposes actions. * * @module tool_dataprivacy/purposesactions * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define("tool_dataprivacy/purposesactions",["jquery","core/ajax","core/notification","core/str","core/modal_factory","core/modal_events"],(function($,Ajax,Notification,Str,ModalFactory,ModalEvents){var ACTIONS_DELETE='[data-action="deletepurpose"]',PurposesActions=function(){this.registerEvents()};return PurposesActions.prototype.registerEvents=function(){$(ACTIONS_DELETE).click((function(e){e.preventDefault();var id=$(this).data("id"),stringkeys=[{key:"deletepurpose",component:"tool_dataprivacy"},{key:"deletepurposetext",component:"tool_dataprivacy",param:$(this).data("name")},{key:"delete"}];Str.get_strings(stringkeys).then((function(langStrings){var title=langStrings[0],confirmMessage=langStrings[1],buttonText=langStrings[2];return ModalFactory.create({title:title,body:confirmMessage,type:ModalFactory.types.SAVE_CANCEL}).then((function(modal){return modal.setSaveButtonText(buttonText),modal.getRoot().on(ModalEvents.save,(function(){var request={methodname:"tool_dataprivacy_delete_purpose",args:{id:id}};Ajax.call([request])[0].done((function(data){data.result?$('tr[data-purposeid="'+id+'"]').remove():Notification.addNotification({message:data.warnings[0].message,type:"error"})})).fail(Notification.exception)})),modal.getRoot().on(ModalEvents.hidden,(function(){modal.destroy()})),modal}))})).done((function(modal){modal.show()})).fail(Notification.exception)}))},{init:function(){return new PurposesActions}}})); //# sourceMappingURL=purposesactions.min.js.map amd/build/add_purpose.min.js.map 0000644 00000020570 15152701722 0012613 0 ustar 00 {"version":3,"file":"add_purpose.min.js","sources":["../src/add_purpose.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Module to add purposes.\n *\n * @module tool_dataprivacy/add_purpose\n * @copyright 2018 David Monllao\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([\n 'jquery',\n 'core/str',\n 'core/ajax',\n 'core/notification',\n 'core/modal_factory',\n 'core/modal_events',\n 'core/fragment',\n 'core_form/changechecker',\n], function(\n $,\n Str,\n Ajax,\n Notification,\n ModalFactory,\n ModalEvents,\n Fragment,\n FormChangeChecker\n) {\n\n var SELECTORS = {\n PURPOSE_LINK: '[data-add-element=\"purpose\"]',\n };\n\n var AddPurpose = function(contextId) {\n this.contextId = contextId;\n\n var stringKeys = [\n {\n key: 'addpurpose',\n component: 'tool_dataprivacy'\n },\n {\n key: 'save',\n component: 'admin'\n }\n ];\n this.strings = Str.get_strings(stringKeys);\n\n this.registerEventListeners();\n };\n\n /**\n * @var {int} contextId\n * @private\n */\n AddPurpose.prototype.contextId = 0;\n\n /**\n * @var {Promise}\n * @private\n */\n AddPurpose.prototype.strings = 0;\n\n AddPurpose.prototype.registerEventListeners = function() {\n\n var trigger = $(SELECTORS.PURPOSE_LINK);\n trigger.on('click', function() {\n return this.strings.then(function(strings) {\n ModalFactory.create({\n type: ModalFactory.types.SAVE_CANCEL,\n title: strings[0],\n body: '',\n }, trigger).done(function(modal) {\n this.setupFormModal(modal, strings[1]);\n }.bind(this));\n }.bind(this))\n .fail(Notification.exception);\n }.bind(this));\n\n };\n\n /**\n * @method getBody\n * @param {Object} formdata\n * @private\n * @return {Promise}\n */\n AddPurpose.prototype.getBody = function(formdata) {\n\n var params = null;\n if (typeof formdata !== \"undefined\") {\n params = {jsonformdata: JSON.stringify(formdata)};\n }\n // Get the content of the modal.\n return Fragment.loadFragment('tool_dataprivacy', 'addpurpose_form', this.contextId, params);\n };\n\n AddPurpose.prototype.setupFormModal = function(modal, saveText) {\n modal.setLarge();\n\n modal.setSaveButtonText(saveText);\n\n // We want to reset the form every time it is opened.\n modal.getRoot().on(ModalEvents.hidden, this.destroy.bind(this));\n\n modal.setBody(this.getBody());\n\n // We catch the modal save event, and use it to submit the form inside the modal.\n // Triggering a form submission will give JS validation scripts a chance to check for errors.\n modal.getRoot().on(ModalEvents.save, this.submitForm.bind(this));\n // We also catch the form submit event and use it to submit the form with ajax.\n modal.getRoot().on('submit', 'form', this.submitFormAjax.bind(this));\n\n this.modal = modal;\n\n modal.show();\n };\n\n /**\n * This triggers a form submission, so that any mform elements can do final tricks before the form submission is processed.\n *\n * @method submitForm\n * @param {Event} e Form submission event.\n * @private\n */\n AddPurpose.prototype.submitForm = function(e) {\n e.preventDefault();\n this.modal.getRoot().find('form').submit();\n };\n\n AddPurpose.prototype.submitFormAjax = function(e) {\n // We don't want to do a real form submission.\n e.preventDefault();\n\n // Convert all the form elements values to a serialised string.\n var formData = this.modal.getRoot().find('form').serialize();\n\n Ajax.call([{\n methodname: 'tool_dataprivacy_create_purpose_form',\n args: {jsonformdata: JSON.stringify(formData)},\n done: function(data) {\n if (data.validationerrors) {\n this.modal.setBody(this.getBody(formData));\n } else {\n this.close();\n }\n }.bind(this),\n\n fail: Notification.exception\n }]);\n };\n\n AddPurpose.prototype.close = function() {\n this.destroy();\n document.location.reload();\n };\n\n AddPurpose.prototype.destroy = function() {\n FormChangeChecker.resetAllFormDirtyStates();\n this.modal.destroy();\n };\n\n AddPurpose.prototype.removeListeners = function() {\n $(SELECTORS.PURPOSE_LINK).off('click');\n };\n\n return /** @alias module:tool_dataprivacy/add_purpose */ {\n getInstance: function(contextId) {\n return new AddPurpose(contextId);\n }\n };\n }\n);\n\n"],"names":["define","$","Str","Ajax","Notification","ModalFactory","ModalEvents","Fragment","FormChangeChecker","SELECTORS","AddPurpose","contextId","strings","get_strings","key","component","registerEventListeners","prototype","trigger","on","this","then","create","type","types","SAVE_CANCEL","title","body","done","modal","setupFormModal","bind","fail","exception","getBody","formdata","params","jsonformdata","JSON","stringify","loadFragment","saveText","setLarge","setSaveButtonText","getRoot","hidden","destroy","setBody","save","submitForm","submitFormAjax","show","e","preventDefault","find","submit","formData","serialize","call","methodname","args","data","validationerrors","close","document","location","reload","resetAllFormDirtyStates","removeListeners","off","getInstance"],"mappings":";;;;;;;AAsBAA,sCAAO,CACH,SACA,WACA,YACA,oBACA,qBACA,oBACA,gBACA,4BACD,SACCC,EACAC,IACAC,KACAC,aACAC,aACAC,YACAC,SACAC,uBAGQC,uBACc,+BAGdC,WAAa,SAASC,gBACjBA,UAAYA,eAYZC,QAAUV,IAAIW,YAVF,CACb,CACIC,IAAK,aACLC,UAAW,oBAEf,CACID,IAAK,OACLC,UAAW,gBAKdC,iCAOTN,WAAWO,UAAUN,UAAY,EAMjCD,WAAWO,UAAUL,QAAU,EAE/BF,WAAWO,UAAUD,uBAAyB,eAEtCE,QAAUjB,EAAEQ,wBAChBS,QAAQC,GAAG,QAAS,kBACTC,KAAKR,QAAQS,KAAK,SAAST,SAC9BP,aAAaiB,OAAO,CAChBC,KAAMlB,aAAamB,MAAMC,YACzBC,MAAOd,QAAQ,GACfe,KAAM,IACPT,SAASU,KAAK,SAASC,YACjBC,eAAeD,MAAOjB,QAAQ,KACrCmB,KAAKX,QACTW,KAAKX,OACNY,KAAK5B,aAAa6B,YACrBF,KAAKX,QAUXV,WAAWO,UAAUiB,QAAU,SAASC,cAEhCC,OAAS,iBACW,IAAbD,WACPC,OAAS,CAACC,aAAcC,KAAKC,UAAUJ,YAGpC5B,SAASiC,aAAa,mBAAoB,kBAAmBpB,KAAKT,UAAWyB,SAGxF1B,WAAWO,UAAUa,eAAiB,SAASD,MAAOY,UAClDZ,MAAMa,WAENb,MAAMc,kBAAkBF,UAGxBZ,MAAMe,UAAUzB,GAAGb,YAAYuC,OAAQzB,KAAK0B,QAAQf,KAAKX,OAEzDS,MAAMkB,QAAQ3B,KAAKc,WAInBL,MAAMe,UAAUzB,GAAGb,YAAY0C,KAAM5B,KAAK6B,WAAWlB,KAAKX,OAE1DS,MAAMe,UAAUzB,GAAG,SAAU,OAAQC,KAAK8B,eAAenB,KAAKX,YAEzDS,MAAQA,MAEbA,MAAMsB,QAUVzC,WAAWO,UAAUgC,WAAa,SAASG,GACvCA,EAAEC,sBACGxB,MAAMe,UAAUU,KAAK,QAAQC,UAGtC7C,WAAWO,UAAUiC,eAAiB,SAASE,GAE3CA,EAAEC,qBAGEG,SAAWpC,KAAKS,MAAMe,UAAUU,KAAK,QAAQG,YAEjDtD,KAAKuD,KAAK,CAAC,CACPC,WAAY,uCACZC,KAAM,CAACvB,aAAcC,KAAKC,UAAUiB,WACpC5B,KAAM,SAASiC,MACPA,KAAKC,sBACAjC,MAAMkB,QAAQ3B,KAAKc,QAAQsB,gBAE3BO,SAEXhC,KAAKX,MAEPY,KAAM5B,aAAa6B,cAI3BvB,WAAWO,UAAU8C,MAAQ,gBACpBjB,UACLkB,SAASC,SAASC,UAGtBxD,WAAWO,UAAU6B,QAAU,WAC3BtC,kBAAkB2D,+BACbtC,MAAMiB,WAGfpC,WAAWO,UAAUmD,gBAAkB,WACnCnE,EAAEQ,wBAAwB4D,IAAI,UAGuB,CACrDC,YAAa,SAAS3D,kBACX,IAAID,WAAWC"} amd/build/data_deletion.min.js 0000644 00000004020 15152701722 0012316 0 ustar 00 /** * Request actions. * * @module tool_dataprivacy/data_deletion * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define("tool_dataprivacy/data_deletion",["jquery","core/ajax","core/notification","core/str","core/modal_factory","core/modal_events"],(function($,Ajax,Notification,Str,ModalFactory,ModalEvents){var ACTIONS_MARK_FOR_DELETION='[data-action="markfordeletion"]',ACTIONS_SELECT_ALL='[data-action="selectall"]',SELECTORS_SELECTCONTEXT=".selectcontext",DataDeletionActions=function(){this.registerEvents()};return DataDeletionActions.prototype.registerEvents=function(){$(ACTIONS_MARK_FOR_DELETION).click((function(e){e.preventDefault();var ids,keys,wsfunction,modalTitle,selectedIds=[];$(SELECTORS_SELECTCONTEXT).each((function(){var checkbox=$(this);checkbox.is(":checked")&&selectedIds.push(checkbox.val())})),ids=selectedIds,keys=[{key:"confirm",component:"moodle"},{key:"confirmcontextdeletion",component:"tool_dataprivacy"}],wsfunction="tool_dataprivacy_confirm_contexts_for_deletion",modalTitle="",Str.get_strings(keys).then((function(langStrings){modalTitle=langStrings[0];var confirmMessage=langStrings[1];return ModalFactory.create({title:modalTitle,body:confirmMessage,type:ModalFactory.types.SAVE_CANCEL})})).then((function(modal){return modal.setSaveButtonText(modalTitle),modal.getRoot().on(ModalEvents.save,(function(){var request={methodname:wsfunction,args:{ids:ids}};Ajax.call([request])[0].done((function(data){data.result?window.location.reload():Notification.addNotification({message:data.warnings[0].message,type:"error"})})).fail(Notification.exception)})),modal.getRoot().on(ModalEvents.hidden,(function(){modal.destroy()})),modal})).done((function(modal){modal.show()})).fail(Notification.exception)})),$(ACTIONS_SELECT_ALL).change((function(e){e.preventDefault(),$(this).is(":checked")?$(SELECTORS_SELECTCONTEXT).attr("checked","checked"):$(SELECTORS_SELECTCONTEXT).removeAttr("checked")}))},DataDeletionActions})); //# sourceMappingURL=data_deletion.min.js.map amd/build/contactdpo.min.js 0000644 00000003045 15152701722 0011666 0 ustar 00 define("tool_dataprivacy/contactdpo",["exports","core_form/modalform","core/notification","core/str","core/toast"],(function(_exports,_modalform,_notification,_str,_toast){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** * Javascript module for contacting the site DPO * * @module tool_dataprivacy/contactdpo * @copyright 2021 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_modalform=_interopRequireDefault(_modalform),_notification=_interopRequireDefault(_notification);const SELECTORS_CONTACT_DPO='[data-action="contactdpo"]';_exports.init=()=>{const triggerElement=document.querySelector(SELECTORS_CONTACT_DPO);triggerElement.addEventListener("click",(event=>{event.preventDefault();const modalForm=new _modalform.default({modalConfig:{title:(0,_str.get_string)("contactdataprotectionofficer","tool_dataprivacy")},formClass:"tool_dataprivacy\\form\\contactdpo",saveButtonText:(0,_str.get_string)("send","tool_dataprivacy"),returnFocus:triggerElement});modalForm.addEventListener(modalForm.events.FORM_SUBMITTED,(event=>{if(event.detail.result)(0,_str.get_string)("requestsubmitted","tool_dataprivacy").then(_toast.add).catch();else{const warningMessages=event.detail.warnings.map((warning=>warning.message));_notification.default.addNotification({type:"error",message:warningMessages.join("<br>")})}})),modalForm.show()}))}})); //# sourceMappingURL=contactdpo.min.js.map amd/build/myrequestactions.min.js 0000644 00000003314 15152701722 0013146 0 ustar 00 define("tool_dataprivacy/myrequestactions",["exports","core/ajax","core/notification","core/pending","core/str"],(function(_exports,_ajax,_notification,_pending,_str){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** * AMD module to enable users to manage their own data requests. * * @module tool_dataprivacy/myrequestactions * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_ajax=_interopRequireDefault(_ajax),_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending);const SELECTORS_CANCEL_REQUEST='[data-action="cancel"][data-requestid]';_exports.init=()=>{document.addEventListener("click",(event=>{const triggerElement=event.target.closest(SELECTORS_CANCEL_REQUEST);if(null===triggerElement)return;event.preventDefault();(0,_str.get_strings)([{key:"cancelrequest",component:"tool_dataprivacy"},{key:"cancelrequestconfirmation",component:"tool_dataprivacy"}]).then((_ref=>{let[cancelRequest,cancelConfirm]=_ref;return _notification.default.confirm(cancelRequest,cancelConfirm,cancelRequest,null,(()=>{const pendingPromise=new _pending.default("tool/dataprivacy:cancelRequest"),request={methodname:"tool_dataprivacy_cancel_data_request",args:{requestid:triggerElement.dataset.requestid}};_ajax.default.call([request])[0].then((response=>(response.result?window.location.reload():_notification.default.addNotification({type:"error",message:response.warnings[0].message}),pendingPromise.resolve()))).catch(_notification.default.exception)}))})).catch()}))}})); //# sourceMappingURL=myrequestactions.min.js.map amd/build/data_request_modal.min.js 0000644 00000003643 15152701722 0013371 0 ustar 00 /** * Request actions. * * @module tool_dataprivacy/data_request_modal * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define("tool_dataprivacy/data_request_modal",["jquery","core/notification","core/custom_interaction_events","core/modal","core/modal_registry","tool_dataprivacy/events"],(function($,Notification,CustomEvents,Modal,ModalRegistry,DataPrivacyEvents){var registered=!1,SELECTORS_APPROVE_BUTTON='[data-action="approve"]',SELECTORS_DENY_BUTTON='[data-action="deny"]',SELECTORS_COMPLETE_BUTTON='[data-action="complete"]',ModalDataRequest=function(root){Modal.call(this,root)};return ModalDataRequest.TYPE="tool_dataprivacy-data_request",(ModalDataRequest.prototype=Object.create(Modal.prototype)).constructor=ModalDataRequest,ModalDataRequest.prototype.registerEventListeners=function(){Modal.prototype.registerEventListeners.call(this),this.getModal().on(CustomEvents.events.activate,SELECTORS_APPROVE_BUTTON,function(e,data){var approveEvent=$.Event(DataPrivacyEvents.approve);this.getRoot().trigger(approveEvent,this),approveEvent.isDefaultPrevented()||(this.hide(),data.originalEvent.preventDefault())}.bind(this)),this.getModal().on(CustomEvents.events.activate,SELECTORS_DENY_BUTTON,function(e,data){var denyEvent=$.Event(DataPrivacyEvents.deny);this.getRoot().trigger(denyEvent,this),denyEvent.isDefaultPrevented()||(this.hide(),data.originalEvent.preventDefault())}.bind(this)),this.getModal().on(CustomEvents.events.activate,SELECTORS_COMPLETE_BUTTON,function(e,data){var completeEvent=$.Event(DataPrivacyEvents.complete);this.getRoot().trigger(completeEvent,this),completeEvent.isDefaultPrevented()||(this.hide(),data.originalEvent.preventDefault())}.bind(this))},registered||(ModalRegistry.register(ModalDataRequest.TYPE,ModalDataRequest,"tool_dataprivacy/data_request_modal"),registered=!0),ModalDataRequest})); //# sourceMappingURL=data_request_modal.min.js.map amd/build/expand_contract.min.js.map 0000644 00000013315 15152701722 0013461 0 ustar 00 {"version":3,"file":"expand_contract.min.js","sources":["../src/expand_contract.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Potential user selector module.\n *\n * @module tool_dataprivacy/expand_contract\n * @copyright 2018 Adrian Greeve\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery', 'core/url', 'core/str', 'core/notification'], function($, url, str, Notification) {\n\n var expandedImage = $('<img alt=\"\" src=\"' + url.imageUrl('t/expanded') + '\"/>');\n var collapsedImage = $('<img alt=\"\" src=\"' + url.imageUrl('t/collapsed') + '\"/>');\n\n /*\n * Class names to apply when expanding/collapsing nodes.\n */\n var CLASSES = {\n EXPAND: 'fa-caret-right',\n COLLAPSE: 'fa-caret-down'\n };\n\n return /** @alias module:tool_dataprivacy/expand-collapse */ {\n /**\n * Expand or collapse a selected node.\n *\n * @param {object} targetnode The node that we want to expand / collapse\n * @param {object} thisnode The node that was clicked.\n */\n expandCollapse: function(targetnode, thisnode) {\n if (targetnode.hasClass('hide')) {\n targetnode.removeClass('hide');\n targetnode.addClass('visible');\n targetnode.attr('aria-expanded', true);\n thisnode.find(':header i.fa').removeClass(CLASSES.EXPAND);\n thisnode.find(':header i.fa').addClass(CLASSES.COLLAPSE);\n thisnode.find(':header img.icon').attr('src', expandedImage.attr('src'));\n } else {\n targetnode.removeClass('visible');\n targetnode.addClass('hide');\n targetnode.attr('aria-expanded', false);\n thisnode.find(':header i.fa').removeClass(CLASSES.COLLAPSE);\n thisnode.find(':header i.fa').addClass(CLASSES.EXPAND);\n thisnode.find(':header img.icon').attr('src', collapsedImage.attr('src'));\n }\n },\n\n /**\n * Expand or collapse all nodes on this page.\n *\n * @param {string} nextstate The next state to change to.\n */\n expandCollapseAll: function(nextstate) {\n var currentstate = (nextstate == 'visible') ? 'hide' : 'visible';\n var ariaexpandedstate = (nextstate == 'visible') ? true : false;\n var iconclassnow = (nextstate == 'visible') ? CLASSES.EXPAND : CLASSES.COLLAPSE;\n var iconclassnext = (nextstate == 'visible') ? CLASSES.COLLAPSE : CLASSES.EXPAND;\n var imagenow = (nextstate == 'visible') ? expandedImage.attr('src') : collapsedImage.attr('src');\n $('.' + currentstate).each(function() {\n $(this).removeClass(currentstate);\n $(this).addClass(nextstate);\n $(this).attr('aria-expanded', ariaexpandedstate);\n });\n $('.tool_dataprivacy-expand-all').data('visibilityState', currentstate);\n\n str.get_string(currentstate, 'tool_dataprivacy').then(function(langString) {\n $('.tool_dataprivacy-expand-all').html(langString);\n return;\n }).catch(Notification.exception);\n\n $(':header i.fa').each(function() {\n $(this).removeClass(iconclassnow);\n $(this).addClass(iconclassnext);\n });\n $(':header img.icon').each(function() {\n $(this).attr('src', imagenow);\n });\n }\n };\n});\n"],"names":["define","$","url","str","Notification","expandedImage","imageUrl","collapsedImage","CLASSES","expandCollapse","targetnode","thisnode","hasClass","removeClass","addClass","attr","find","expandCollapseAll","nextstate","currentstate","ariaexpandedstate","iconclassnow","iconclassnext","imagenow","each","this","data","get_string","then","langString","html","catch","exception"],"mappings":";;;;;;;AAuBAA,0CAAO,CAAC,SAAU,WAAY,WAAY,sBAAsB,SAASC,EAAGC,IAAKC,IAAKC,kBAE9EC,cAAgBJ,EAAE,oBAAsBC,IAAII,SAAS,cAAgB,OACrEC,eAAiBN,EAAE,oBAAsBC,IAAII,SAAS,eAAiB,OAKvEE,eACQ,iBADRA,iBAEU,sBAG+C,CAOzDC,eAAgB,SAASC,WAAYC,UAC7BD,WAAWE,SAAS,SACpBF,WAAWG,YAAY,QACvBH,WAAWI,SAAS,WACpBJ,WAAWK,KAAK,iBAAiB,GACjCJ,SAASK,KAAK,gBAAgBH,YAAYL,gBAC1CG,SAASK,KAAK,gBAAgBF,SAASN,kBACvCG,SAASK,KAAK,oBAAoBD,KAAK,MAAOV,cAAcU,KAAK,UAEjEL,WAAWG,YAAY,WACvBH,WAAWI,SAAS,QACpBJ,WAAWK,KAAK,iBAAiB,GACjCJ,SAASK,KAAK,gBAAgBH,YAAYL,kBAC1CG,SAASK,KAAK,gBAAgBF,SAASN,gBACvCG,SAASK,KAAK,oBAAoBD,KAAK,MAAOR,eAAeQ,KAAK,UAS1EE,kBAAmB,SAASC,eACpBC,aAA6B,WAAbD,UAA0B,OAAS,UACnDE,kBAAkC,WAAbF,UACrBG,aAA6B,WAAbH,UAA0BV,eAAiBA,iBAC3Dc,cAA8B,WAAbJ,UAA0BV,iBAAmBA,eAC9De,SAAyB,WAAbL,UAA0Bb,cAAcU,KAAK,OAASR,eAAeQ,KAAK,OAC1Fd,EAAE,IAAMkB,cAAcK,MAAK,WACvBvB,EAAEwB,MAAMZ,YAAYM,cACpBlB,EAAEwB,MAAMX,SAASI,WACjBjB,EAAEwB,MAAMV,KAAK,gBAAiBK,sBAElCnB,EAAE,gCAAgCyB,KAAK,kBAAmBP,cAE1DhB,IAAIwB,WAAWR,aAAc,oBAAoBS,MAAK,SAASC,YAC3D5B,EAAE,gCAAgC6B,KAAKD,eAExCE,MAAM3B,aAAa4B,WAEtB/B,EAAE,gBAAgBuB,MAAK,WACnBvB,EAAEwB,MAAMZ,YAAYQ,cACpBpB,EAAEwB,MAAMX,SAASQ,kBAErBrB,EAAE,oBAAoBuB,MAAK,WACvBvB,EAAEwB,MAAMV,KAAK,MAAOQ"} amd/build/requestactions.min.js.map 0000644 00000045356 15152701722 0013370 0 ustar 00 {"version":3,"file":"requestactions.min.js","sources":["../src/requestactions.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Request actions.\n *\n * @module tool_dataprivacy/requestactions\n * @copyright 2018 Jun Pataleta\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([\n 'jquery',\n 'core/ajax',\n 'core/notification',\n 'core/str',\n 'core/modal_factory',\n 'core/modal_events',\n 'core/templates',\n 'tool_dataprivacy/data_request_modal',\n 'tool_dataprivacy/events'],\nfunction($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, ModalDataRequest, DataPrivacyEvents) {\n\n /**\n * List of action selectors.\n *\n * @type {{APPROVE_REQUEST: string}}\n * @type {{DENY_REQUEST: string}}\n * @type {{VIEW_REQUEST: string}}\n * @type {{MARK_COMPLETE: string}}\n * @type {{CHANGE_BULK_ACTION: string}}\n * @type {{CONFIRM_BULK_ACTION: string}}\n * @type {{SELECT_ALL: string}}\n */\n var ACTIONS = {\n APPROVE_REQUEST: '[data-action=\"approve\"]',\n DENY_REQUEST: '[data-action=\"deny\"]',\n VIEW_REQUEST: '[data-action=\"view\"]',\n MARK_COMPLETE: '[data-action=\"complete\"]',\n CHANGE_BULK_ACTION: '[id=\"bulk-action\"]',\n CONFIRM_BULK_ACTION: '[id=\"confirm-bulk-action\"]',\n SELECT_ALL: '[data-action=\"selectall\"]'\n };\n\n /**\n * List of available bulk actions.\n *\n * @type {{APPROVE: number}}\n * @type {{DENY: number}}\n */\n var BULK_ACTIONS = {\n APPROVE: 1,\n DENY: 2\n };\n\n /**\n * List of selectors.\n *\n * @type {{SELECT_REQUEST: string}}\n */\n var SELECTORS = {\n SELECT_REQUEST: '.selectrequests'\n };\n\n /**\n * RequestActions class.\n */\n var RequestActions = function() {\n this.registerEvents();\n };\n\n /**\n * Register event listeners.\n */\n RequestActions.prototype.registerEvents = function() {\n $(ACTIONS.VIEW_REQUEST).click(function(e) {\n e.preventDefault();\n\n var requestId = $(this).data('requestid');\n\n // Cancel the request.\n var params = {\n 'requestid': requestId\n };\n\n var request = {\n methodname: 'tool_dataprivacy_get_data_request',\n args: params\n };\n\n var promises = Ajax.call([request]);\n $.when(promises[0]).then(function(data) {\n if (data.result) {\n return data.result;\n }\n // Fail.\n Notification.addNotification({\n message: data.warnings[0].message,\n type: 'error'\n });\n return false;\n\n }).then(function(data) {\n var body = Templates.render('tool_dataprivacy/request_details', data);\n var templateContext = {\n approvedeny: data.approvedeny,\n canmarkcomplete: data.canmarkcomplete\n };\n return ModalFactory.create({\n title: data.typename,\n body: body,\n type: ModalDataRequest.TYPE,\n large: true,\n templateContext: templateContext\n });\n\n }).then(function(modal) {\n // Handle approve event.\n modal.getRoot().on(DataPrivacyEvents.approve, function() {\n showConfirmation(DataPrivacyEvents.approve, approveEventWsData(requestId));\n });\n\n // Handle deny event.\n modal.getRoot().on(DataPrivacyEvents.deny, function() {\n showConfirmation(DataPrivacyEvents.deny, denyEventWsData(requestId));\n });\n\n // Handle send event.\n modal.getRoot().on(DataPrivacyEvents.complete, function() {\n var params = {\n 'requestid': requestId\n };\n handleSave('tool_dataprivacy_mark_complete', params);\n });\n\n // Handle hidden event.\n modal.getRoot().on(ModalEvents.hidden, function() {\n // Destroy when hidden.\n modal.destroy();\n });\n\n // Show the modal!\n modal.show();\n\n return;\n\n }).catch(Notification.exception);\n });\n\n $(ACTIONS.APPROVE_REQUEST).click(function(e) {\n e.preventDefault();\n\n var requestId = $(this).data('requestid');\n showConfirmation(DataPrivacyEvents.approve, approveEventWsData(requestId));\n });\n\n $(ACTIONS.DENY_REQUEST).click(function(e) {\n e.preventDefault();\n\n var requestId = $(this).data('requestid');\n showConfirmation(DataPrivacyEvents.deny, denyEventWsData(requestId));\n });\n\n $(ACTIONS.MARK_COMPLETE).click(function(e) {\n e.preventDefault();\n\n var requestId = $(this).data('requestid');\n showConfirmation(DataPrivacyEvents.complete, completeEventWsData(requestId));\n });\n\n $(ACTIONS.CONFIRM_BULK_ACTION).click(function() {\n var requestIds = [];\n var actionEvent = '';\n var wsdata = {};\n var bulkActionKeys = [\n {\n key: 'selectbulkaction',\n component: 'tool_dataprivacy'\n },\n {\n key: 'selectdatarequests',\n component: 'tool_dataprivacy'\n },\n {\n key: 'ok'\n }\n ];\n\n var bulkaction = parseInt($('#bulk-action').val());\n\n if (bulkaction != BULK_ACTIONS.APPROVE && bulkaction != BULK_ACTIONS.DENY) {\n Str.get_strings(bulkActionKeys).done(function(langStrings) {\n Notification.alert('', langStrings[0], langStrings[2]);\n }).fail(Notification.exception);\n\n return;\n }\n\n $(\".selectrequests:checked\").each(function() {\n requestIds.push($(this).val());\n });\n\n if (requestIds.length < 1) {\n Str.get_strings(bulkActionKeys).done(function(langStrings) {\n Notification.alert('', langStrings[1], langStrings[2]);\n }).fail(Notification.exception);\n\n return;\n }\n\n switch (bulkaction) {\n case BULK_ACTIONS.APPROVE:\n actionEvent = DataPrivacyEvents.bulkApprove;\n wsdata = bulkApproveEventWsData(requestIds);\n break;\n case BULK_ACTIONS.DENY:\n actionEvent = DataPrivacyEvents.bulkDeny;\n wsdata = bulkDenyEventWsData(requestIds);\n }\n\n showConfirmation(actionEvent, wsdata);\n });\n\n $(ACTIONS.SELECT_ALL).change(function(e) {\n e.preventDefault();\n\n var selectAll = $(this).is(':checked');\n $(SELECTORS.SELECT_REQUEST).prop('checked', selectAll);\n });\n };\n\n /**\n * Return the webservice data for the approve request action.\n *\n * @param {Number} requestId The ID of the request.\n * @return {Object}\n */\n function approveEventWsData(requestId) {\n return {\n 'wsfunction': 'tool_dataprivacy_approve_data_request',\n 'wsparams': {'requestid': requestId}\n };\n }\n\n /**\n * Return the webservice data for the bulk approve request action.\n *\n * @param {Array} requestIds The array of request ID's.\n * @return {Object}\n */\n function bulkApproveEventWsData(requestIds) {\n return {\n 'wsfunction': 'tool_dataprivacy_bulk_approve_data_requests',\n 'wsparams': {'requestids': requestIds}\n };\n }\n\n /**\n * Return the webservice data for the deny request action.\n *\n * @param {Number} requestId The ID of the request.\n * @return {Object}\n */\n function denyEventWsData(requestId) {\n return {\n 'wsfunction': 'tool_dataprivacy_deny_data_request',\n 'wsparams': {'requestid': requestId}\n };\n }\n\n /**\n * Return the webservice data for the bulk deny request action.\n *\n * @param {Array} requestIds The array of request ID's.\n * @return {Object}\n */\n function bulkDenyEventWsData(requestIds) {\n return {\n 'wsfunction': 'tool_dataprivacy_bulk_deny_data_requests',\n 'wsparams': {'requestids': requestIds}\n };\n }\n\n /**\n * Return the webservice data for the complete request action.\n *\n * @param {Number} requestId The ID of the request.\n * @return {Object}\n */\n function completeEventWsData(requestId) {\n return {\n 'wsfunction': 'tool_dataprivacy_mark_complete',\n 'wsparams': {'requestid': requestId}\n };\n }\n\n /**\n * Show the confirmation dialogue.\n *\n * @param {String} action The action name.\n * @param {Object} wsdata Object containing ws data.\n */\n function showConfirmation(action, wsdata) {\n var keys = [];\n\n switch (action) {\n case DataPrivacyEvents.approve:\n keys = [\n {\n key: 'approverequest',\n component: 'tool_dataprivacy'\n },\n {\n key: 'confirmapproval',\n component: 'tool_dataprivacy'\n }\n ];\n break;\n case DataPrivacyEvents.bulkApprove:\n keys = [\n {\n key: 'bulkapproverequests',\n component: 'tool_dataprivacy'\n },\n {\n key: 'confirmbulkapproval',\n component: 'tool_dataprivacy'\n }\n ];\n break;\n case DataPrivacyEvents.deny:\n keys = [\n {\n key: 'denyrequest',\n component: 'tool_dataprivacy'\n },\n {\n key: 'confirmdenial',\n component: 'tool_dataprivacy'\n }\n ];\n break;\n case DataPrivacyEvents.bulkDeny:\n keys = [\n {\n key: 'bulkdenyrequests',\n component: 'tool_dataprivacy'\n },\n {\n key: 'confirmbulkdenial',\n component: 'tool_dataprivacy'\n }\n ];\n break;\n case DataPrivacyEvents.complete:\n keys = [\n {\n key: 'markcomplete',\n component: 'tool_dataprivacy'\n },\n {\n key: 'confirmcompletion',\n component: 'tool_dataprivacy'\n }\n ];\n break;\n }\n\n var modalTitle = '';\n Str.get_strings(keys).then(function(langStrings) {\n modalTitle = langStrings[0];\n var confirmMessage = langStrings[1];\n return ModalFactory.create({\n title: modalTitle,\n body: confirmMessage,\n type: ModalFactory.types.SAVE_CANCEL\n });\n }).then(function(modal) {\n modal.setSaveButtonText(modalTitle);\n\n // Handle save event.\n modal.getRoot().on(ModalEvents.save, function() {\n handleSave(wsdata.wsfunction, wsdata.wsparams);\n });\n\n // Handle hidden event.\n modal.getRoot().on(ModalEvents.hidden, function() {\n // Destroy when hidden.\n modal.destroy();\n });\n\n modal.show();\n\n return;\n\n }).catch(Notification.exception);\n }\n\n /**\n * Calls a web service function and reloads the page on success and shows a notification.\n * Displays an error notification, otherwise.\n *\n * @param {String} wsfunction The web service function to call.\n * @param {Object} params The parameters for the web service functoon.\n */\n function handleSave(wsfunction, params) {\n // Confirm the request.\n var request = {\n methodname: wsfunction,\n args: params\n };\n\n Ajax.call([request])[0].done(function(data) {\n if (data.result) {\n // On success, reload the page so that the data request table will be updated.\n // TODO: Probably in the future, better to reload the table or the target data request via AJAX.\n window.location.reload();\n } else {\n // Add the notification.\n Notification.addNotification({\n message: data.warnings[0].message,\n type: 'error'\n });\n }\n }).fail(Notification.exception);\n }\n\n return RequestActions;\n});\n"],"names":["define","$","Ajax","Notification","Str","ModalFactory","ModalEvents","Templates","ModalDataRequest","DataPrivacyEvents","ACTIONS","BULK_ACTIONS","SELECTORS","RequestActions","registerEvents","approveEventWsData","requestId","denyEventWsData","showConfirmation","action","wsdata","keys","approve","key","component","bulkApprove","deny","bulkDeny","complete","modalTitle","get_strings","then","langStrings","confirmMessage","create","title","body","type","types","SAVE_CANCEL","modal","setSaveButtonText","getRoot","on","save","handleSave","wsfunction","wsparams","hidden","destroy","show","catch","exception","params","request","methodname","args","call","done","data","result","window","location","reload","addNotification","message","warnings","fail","prototype","click","e","preventDefault","this","promises","when","render","templateContext","approvedeny","canmarkcomplete","typename","TYPE","large","completeEventWsData","requestIds","actionEvent","bulkActionKeys","bulkaction","parseInt","val","each","push","length","alert","bulkApproveEventWsData","bulkDenyEventWsData","change","selectAll","is","prop"],"mappings":";;;;;;;AAsBAA,yCAAO,CACH,SACA,YACA,oBACA,WACA,qBACA,oBACA,iBACA,sCACA,4BACJ,SAASC,EAAGC,KAAMC,aAAcC,IAAKC,aAAcC,YAAaC,UAAWC,iBAAkBC,uBAarFC,wBACiB,0BADjBA,qBAEc,uBAFdA,qBAGc,uBAHdA,sBAIe,2BAJfA,4BAMqB,6BANrBA,mBAOY,4BASZC,qBACS,EADTA,kBAEM,EAQNC,yBACgB,kBAMhBC,eAAiB,gBACZC,2BAyKAC,mBAAmBC,iBACjB,YACW,iDACF,WAAcA,qBAuBzBC,gBAAgBD,iBACd,YACW,8CACF,WAAcA,qBAoCzBE,iBAAiBC,OAAQC,YAC1BC,KAAO,UAEHF,aACCV,kBAAkBa,QACnBD,KAAO,CACH,CACIE,IAAK,iBACLC,UAAW,oBAEf,CACID,IAAK,kBACLC,UAAW,gCAIlBf,kBAAkBgB,YACnBJ,KAAO,CACH,CACIE,IAAK,sBACLC,UAAW,oBAEf,CACID,IAAK,sBACLC,UAAW,gCAIlBf,kBAAkBiB,KACnBL,KAAO,CACH,CACIE,IAAK,cACLC,UAAW,oBAEf,CACID,IAAK,gBACLC,UAAW,gCAIlBf,kBAAkBkB,SACnBN,KAAO,CACH,CACIE,IAAK,mBACLC,UAAW,oBAEf,CACID,IAAK,oBACLC,UAAW,gCAIlBf,kBAAkBmB,SACnBP,KAAO,CACH,CACIE,IAAK,eACLC,UAAW,oBAEf,CACID,IAAK,oBACLC,UAAW,yBAMvBK,WAAa,GACjBzB,IAAI0B,YAAYT,MAAMU,MAAK,SAASC,aAChCH,WAAaG,YAAY,OACrBC,eAAiBD,YAAY,UAC1B3B,aAAa6B,OAAO,CACvBC,MAAON,WACPO,KAAMH,eACNI,KAAMhC,aAAaiC,MAAMC,iBAE9BR,MAAK,SAASS,OACbA,MAAMC,kBAAkBZ,YAGxBW,MAAME,UAAUC,GAAGrC,YAAYsC,MAAM,WACjCC,WAAWzB,OAAO0B,WAAY1B,OAAO2B,aAIzCP,MAAME,UAAUC,GAAGrC,YAAY0C,QAAQ,WAEnCR,MAAMS,aAGVT,MAAMU,UAIPC,MAAMhD,aAAaiD,oBAUjBP,WAAWC,WAAYO,YAExBC,QAAU,CACVC,WAAYT,WACZU,KAAMH,QAGVnD,KAAKuD,KAAK,CAACH,UAAU,GAAGI,MAAK,SAASC,MAC9BA,KAAKC,OAGLC,OAAOC,SAASC,SAGhB5D,aAAa6D,gBAAgB,CACzBC,QAASN,KAAKO,SAAS,GAAGD,QAC1B5B,KAAM,aAGf8B,KAAKhE,aAAaiD,kBA9VzBvC,eAAeuD,UAAUtD,eAAiB,WACtCb,EAAES,sBAAsB2D,OAAM,SAASC,GACnCA,EAAEC,qBAEEvD,UAAYf,EAAEuE,MAAMb,KAAK,aAOzBL,QAAU,CACVC,WAAY,oCACZC,KANS,WACIxC,YAQbyD,SAAWvE,KAAKuD,KAAK,CAACH,UAC1BrD,EAAEyE,KAAKD,SAAS,IAAI1C,MAAK,SAAS4B,aAC1BA,KAAKC,OACED,KAAKC,QAGhBzD,aAAa6D,gBAAgB,CACzBC,QAASN,KAAKO,SAAS,GAAGD,QAC1B5B,KAAM,WAEH,MAERN,MAAK,SAAS4B,UACTvB,KAAO7B,UAAUoE,OAAO,mCAAoChB,MAC5DiB,gBAAkB,CAClBC,YAAalB,KAAKkB,YAClBC,gBAAiBnB,KAAKmB,wBAEnBzE,aAAa6B,OAAO,CACvBC,MAAOwB,KAAKoB,SACZ3C,KAAMA,KACNC,KAAM7B,iBAAiBwE,KACvBC,OAAO,EACPL,gBAAiBA,qBAGtB7C,MAAK,SAASS,OAEbA,MAAME,UAAUC,GAAGlC,kBAAkBa,SAAS,WAC1CJ,iBAAiBT,kBAAkBa,QAASP,mBAAmBC,eAInEwB,MAAME,UAAUC,GAAGlC,kBAAkBiB,MAAM,WACvCR,iBAAiBT,kBAAkBiB,KAAMT,gBAAgBD,eAI7DwB,MAAME,UAAUC,GAAGlC,kBAAkBmB,UAAU,WAI3CiB,WAAW,iCAHE,WACI7B,eAMrBwB,MAAME,UAAUC,GAAGrC,YAAY0C,QAAQ,WAEnCR,MAAMS,aAIVT,MAAMU,UAIPC,MAAMhD,aAAaiD,cAG1BnD,EAAES,yBAAyB2D,OAAM,SAASC,GACtCA,EAAEC,qBAEEvD,UAAYf,EAAEuE,MAAMb,KAAK,aAC7BzC,iBAAiBT,kBAAkBa,QAASP,mBAAmBC,eAGnEf,EAAES,sBAAsB2D,OAAM,SAASC,GACnCA,EAAEC,qBAEEvD,UAAYf,EAAEuE,MAAMb,KAAK,aAC7BzC,iBAAiBT,kBAAkBiB,KAAMT,gBAAgBD,eAG7Df,EAAES,uBAAuB2D,OAAM,SAASC,GACpCA,EAAEC,qBAEEvD,UAAYf,EAAEuE,MAAMb,KAAK,aAC7BzC,iBAAiBT,kBAAkBmB,kBA0HdZ,iBAClB,YACW,0CACF,WAAcA,YA7HmBkE,CAAoBlE,eAGrEf,EAAES,6BAA6B2D,OAAM,eAC7Bc,WAAa,GACbC,YAAc,GACdhE,OAAS,GACTiE,eAAiB,CACjB,CACI9D,IAAK,mBACLC,UAAW,oBAEf,CACID,IAAK,qBACLC,UAAW,oBAEf,CACID,IAAK,OAIT+D,WAAaC,SAAStF,EAAE,gBAAgBuF,UAExCF,YAAc3E,sBAAwB2E,YAAc3E,qBAQxDV,EAAE,2BAA2BwF,MAAK,WAC9BN,WAAWO,KAAKzF,EAAEuE,MAAMgB,UAGxBL,WAAWQ,OAAS,EACpBvF,IAAI0B,YAAYuD,gBAAgB3B,MAAK,SAAS1B,aAC1C7B,aAAayF,MAAM,GAAI5D,YAAY,GAAIA,YAAY,OACpDmC,KAAKhE,aAAaiD,uBAKjBkC,iBACC3E,qBACDyE,YAAc3E,kBAAkBgB,YAChCL,gBAqCgB+D,kBACrB,YACW,uDACF,YAAeA,aAxCVU,CAAuBV,uBAE/BxE,kBACDyE,YAAc3E,kBAAkBkB,SAChCP,gBA2Da+D,kBAClB,YACW,oDACF,YAAeA,aA9DVW,CAAoBX,YAGrCjE,iBAAiBkE,YAAahE,aA7B1BhB,IAAI0B,YAAYuD,gBAAgB3B,MAAK,SAAS1B,aAC1C7B,aAAayF,MAAM,GAAI5D,YAAY,GAAIA,YAAY,OACpDmC,KAAKhE,aAAaiD,cA8B7BnD,EAAES,oBAAoBqF,QAAO,SAASzB,GAClCA,EAAEC,qBAEEyB,UAAY/F,EAAEuE,MAAMyB,GAAG,YAC3BhG,EAAEW,0BAA0BsF,KAAK,UAAWF,eAwM7CnF"} amd/build/add_purpose.min.js 0000644 00000005101 15152701722 0012030 0 ustar 00 /** * Module to add purposes. * * @module tool_dataprivacy/add_purpose * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define("tool_dataprivacy/add_purpose",["jquery","core/str","core/ajax","core/notification","core/modal_factory","core/modal_events","core/fragment","core_form/changechecker"],(function($,Str,Ajax,Notification,ModalFactory,ModalEvents,Fragment,FormChangeChecker){var SELECTORS_PURPOSE_LINK='[data-add-element="purpose"]',AddPurpose=function(contextId){this.contextId=contextId;this.strings=Str.get_strings([{key:"addpurpose",component:"tool_dataprivacy"},{key:"save",component:"admin"}]),this.registerEventListeners()};return AddPurpose.prototype.contextId=0,AddPurpose.prototype.strings=0,AddPurpose.prototype.registerEventListeners=function(){var trigger=$(SELECTORS_PURPOSE_LINK);trigger.on("click",function(){return this.strings.then(function(strings){ModalFactory.create({type:ModalFactory.types.SAVE_CANCEL,title:strings[0],body:""},trigger).done(function(modal){this.setupFormModal(modal,strings[1])}.bind(this))}.bind(this)).fail(Notification.exception)}.bind(this))},AddPurpose.prototype.getBody=function(formdata){var params=null;return void 0!==formdata&&(params={jsonformdata:JSON.stringify(formdata)}),Fragment.loadFragment("tool_dataprivacy","addpurpose_form",this.contextId,params)},AddPurpose.prototype.setupFormModal=function(modal,saveText){modal.setLarge(),modal.setSaveButtonText(saveText),modal.getRoot().on(ModalEvents.hidden,this.destroy.bind(this)),modal.setBody(this.getBody()),modal.getRoot().on(ModalEvents.save,this.submitForm.bind(this)),modal.getRoot().on("submit","form",this.submitFormAjax.bind(this)),this.modal=modal,modal.show()},AddPurpose.prototype.submitForm=function(e){e.preventDefault(),this.modal.getRoot().find("form").submit()},AddPurpose.prototype.submitFormAjax=function(e){e.preventDefault();var formData=this.modal.getRoot().find("form").serialize();Ajax.call([{methodname:"tool_dataprivacy_create_purpose_form",args:{jsonformdata:JSON.stringify(formData)},done:function(data){data.validationerrors?this.modal.setBody(this.getBody(formData)):this.close()}.bind(this),fail:Notification.exception}])},AddPurpose.prototype.close=function(){this.destroy(),document.location.reload()},AddPurpose.prototype.destroy=function(){FormChangeChecker.resetAllFormDirtyStates(),this.modal.destroy()},AddPurpose.prototype.removeListeners=function(){$(SELECTORS_PURPOSE_LINK).off("click")},{getInstance:function(contextId){return new AddPurpose(contextId)}}})); //# sourceMappingURL=add_purpose.min.js.map amd/build/add_category.min.js.map 0000644 00000020620 15152701722 0012727 0 ustar 00 {"version":3,"file":"add_category.min.js","sources":["../src/add_category.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Module to add categories.\n *\n * @module tool_dataprivacy/add_category\n * @copyright 2018 David Monllao\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([\n 'jquery',\n 'core/str',\n 'core/ajax',\n 'core/notification',\n 'core/modal_factory',\n 'core/modal_events',\n 'core/fragment',\n 'core_form/changechecker',\n], function(\n $,\n Str,\n Ajax,\n Notification,\n ModalFactory,\n ModalEvents,\n Fragment,\n FormChangeChecker\n) {\n\n var SELECTORS = {\n CATEGORY_LINK: '[data-add-element=\"category\"]',\n };\n\n var AddCategory = function(contextId) {\n this.contextId = contextId;\n\n var stringKeys = [\n {\n key: 'addcategory',\n component: 'tool_dataprivacy'\n },\n {\n key: 'save',\n component: 'admin'\n }\n ];\n this.strings = Str.get_strings(stringKeys);\n\n this.registerEventListeners();\n };\n\n /**\n * @var {int} contextId\n * @private\n */\n AddCategory.prototype.contextId = 0;\n\n /**\n * @var {Promise}\n * @private\n */\n AddCategory.prototype.strings = 0;\n\n AddCategory.prototype.registerEventListeners = function() {\n\n var trigger = $(SELECTORS.CATEGORY_LINK);\n trigger.on('click', function() {\n return this.strings.then(function(strings) {\n ModalFactory.create({\n type: ModalFactory.types.SAVE_CANCEL,\n title: strings[0],\n body: '',\n }, trigger).done(function(modal) {\n this.setupFormModal(modal, strings[1]);\n }.bind(this));\n }.bind(this))\n .fail(Notification.exception);\n }.bind(this));\n\n };\n\n /**\n * @method getBody\n * @param {Object} formdata\n * @private\n * @return {Promise}\n */\n AddCategory.prototype.getBody = function(formdata) {\n\n var params = null;\n if (typeof formdata !== \"undefined\") {\n params = {jsonformdata: JSON.stringify(formdata)};\n }\n // Get the content of the modal.\n return Fragment.loadFragment('tool_dataprivacy', 'addcategory_form', this.contextId, params);\n };\n\n AddCategory.prototype.setupFormModal = function(modal, saveText) {\n modal.setLarge();\n\n modal.setSaveButtonText(saveText);\n\n // We want to reset the form every time it is opened.\n modal.getRoot().on(ModalEvents.hidden, this.destroy.bind(this));\n\n modal.setBody(this.getBody());\n\n // We catch the modal save event, and use it to submit the form inside the modal.\n // Triggering a form submission will give JS validation scripts a chance to check for errors.\n modal.getRoot().on(ModalEvents.save, this.submitForm.bind(this));\n // We also catch the form submit event and use it to submit the form with ajax.\n modal.getRoot().on('submit', 'form', this.submitFormAjax.bind(this));\n\n this.modal = modal;\n\n modal.show();\n };\n\n /**\n * This triggers a form submission, so that any mform elements can do final tricks before the form submission is processed.\n *\n * @method submitForm\n * @param {Event} e Form submission event.\n * @private\n */\n AddCategory.prototype.submitForm = function(e) {\n e.preventDefault();\n this.modal.getRoot().find('form').submit();\n };\n\n AddCategory.prototype.submitFormAjax = function(e) {\n // We don't want to do a real form submission.\n e.preventDefault();\n\n // Convert all the form elements values to a serialised string.\n var formData = this.modal.getRoot().find('form').serialize();\n\n Ajax.call([{\n methodname: 'tool_dataprivacy_create_category_form',\n args: {jsonformdata: JSON.stringify(formData)},\n done: function(data) {\n if (data.validationerrors) {\n this.modal.setBody(this.getBody(formData));\n } else {\n this.close();\n }\n }.bind(this),\n fail: Notification.exception\n }]);\n };\n\n AddCategory.prototype.close = function() {\n this.destroy();\n document.location.reload();\n };\n\n AddCategory.prototype.destroy = function() {\n FormChangeChecker.resetAllFormDirtyStates();\n this.modal.destroy();\n };\n\n AddCategory.prototype.removeListeners = function() {\n $(SELECTORS.CATEGORY_LINK).off('click');\n };\n\n return /** @alias module:tool_dataprivacy/add_category */ {\n getInstance: function(contextId) {\n return new AddCategory(contextId);\n }\n };\n }\n);\n\n"],"names":["define","$","Str","Ajax","Notification","ModalFactory","ModalEvents","Fragment","FormChangeChecker","SELECTORS","AddCategory","contextId","strings","get_strings","key","component","registerEventListeners","prototype","trigger","on","this","then","create","type","types","SAVE_CANCEL","title","body","done","modal","setupFormModal","bind","fail","exception","getBody","formdata","params","jsonformdata","JSON","stringify","loadFragment","saveText","setLarge","setSaveButtonText","getRoot","hidden","destroy","setBody","save","submitForm","submitFormAjax","show","e","preventDefault","find","submit","formData","serialize","call","methodname","args","data","validationerrors","close","document","location","reload","resetAllFormDirtyStates","removeListeners","off","getInstance"],"mappings":";;;;;;;AAsBAA,uCAAO,CACH,SACA,WACA,YACA,oBACA,qBACA,oBACA,gBACA,4BACD,SACCC,EACAC,IACAC,KACAC,aACAC,aACAC,YACAC,SACAC,uBAGQC,wBACe,gCAGfC,YAAc,SAASC,gBAClBA,UAAYA,eAYZC,QAAUV,IAAIW,YAVF,CACb,CACIC,IAAK,cACLC,UAAW,oBAEf,CACID,IAAK,OACLC,UAAW,gBAKdC,iCAOTN,YAAYO,UAAUN,UAAY,EAMlCD,YAAYO,UAAUL,QAAU,EAEhCF,YAAYO,UAAUD,uBAAyB,eAEvCE,QAAUjB,EAAEQ,yBAChBS,QAAQC,GAAG,QAAS,kBACTC,KAAKR,QAAQS,KAAK,SAAST,SAC9BP,aAAaiB,OAAO,CAChBC,KAAMlB,aAAamB,MAAMC,YACzBC,MAAOd,QAAQ,GACfe,KAAM,IACPT,SAASU,KAAK,SAASC,YACjBC,eAAeD,MAAOjB,QAAQ,KACrCmB,KAAKX,QACTW,KAAKX,OACNY,KAAK5B,aAAa6B,YACrBF,KAAKX,QAUXV,YAAYO,UAAUiB,QAAU,SAASC,cAEjCC,OAAS,iBACW,IAAbD,WACPC,OAAS,CAACC,aAAcC,KAAKC,UAAUJ,YAGpC5B,SAASiC,aAAa,mBAAoB,mBAAoBpB,KAAKT,UAAWyB,SAGzF1B,YAAYO,UAAUa,eAAiB,SAASD,MAAOY,UACnDZ,MAAMa,WAENb,MAAMc,kBAAkBF,UAGxBZ,MAAMe,UAAUzB,GAAGb,YAAYuC,OAAQzB,KAAK0B,QAAQf,KAAKX,OAEzDS,MAAMkB,QAAQ3B,KAAKc,WAInBL,MAAMe,UAAUzB,GAAGb,YAAY0C,KAAM5B,KAAK6B,WAAWlB,KAAKX,OAE1DS,MAAMe,UAAUzB,GAAG,SAAU,OAAQC,KAAK8B,eAAenB,KAAKX,YAEzDS,MAAQA,MAEbA,MAAMsB,QAUVzC,YAAYO,UAAUgC,WAAa,SAASG,GACxCA,EAAEC,sBACGxB,MAAMe,UAAUU,KAAK,QAAQC,UAGtC7C,YAAYO,UAAUiC,eAAiB,SAASE,GAE5CA,EAAEC,qBAGEG,SAAWpC,KAAKS,MAAMe,UAAUU,KAAK,QAAQG,YAEjDtD,KAAKuD,KAAK,CAAC,CACPC,WAAY,wCACZC,KAAM,CAACvB,aAAcC,KAAKC,UAAUiB,WACpC5B,KAAM,SAASiC,MACPA,KAAKC,sBACAjC,MAAMkB,QAAQ3B,KAAKc,QAAQsB,gBAE3BO,SAEXhC,KAAKX,MACPY,KAAM5B,aAAa6B,cAI3BvB,YAAYO,UAAU8C,MAAQ,gBACrBjB,UACLkB,SAASC,SAASC,UAGtBxD,YAAYO,UAAU6B,QAAU,WAC5BtC,kBAAkB2D,+BACbtC,MAAMiB,WAGfpC,YAAYO,UAAUmD,gBAAkB,WACpCnE,EAAEQ,yBAAyB4D,IAAI,UAGuB,CACtDC,YAAa,SAAS3D,kBACX,IAAID,YAAYC"} amd/build/requestactions.min.js 0000644 00000013734 15152701722 0012607 0 ustar 00 /** * Request actions. * * @module tool_dataprivacy/requestactions * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define("tool_dataprivacy/requestactions",["jquery","core/ajax","core/notification","core/str","core/modal_factory","core/modal_events","core/templates","tool_dataprivacy/data_request_modal","tool_dataprivacy/events"],(function($,Ajax,Notification,Str,ModalFactory,ModalEvents,Templates,ModalDataRequest,DataPrivacyEvents){var ACTIONS_APPROVE_REQUEST='[data-action="approve"]',ACTIONS_DENY_REQUEST='[data-action="deny"]',ACTIONS_VIEW_REQUEST='[data-action="view"]',ACTIONS_MARK_COMPLETE='[data-action="complete"]',ACTIONS_CONFIRM_BULK_ACTION='[id="confirm-bulk-action"]',ACTIONS_SELECT_ALL='[data-action="selectall"]',BULK_ACTIONS_APPROVE=1,BULK_ACTIONS_DENY=2,SELECTORS_SELECT_REQUEST=".selectrequests",RequestActions=function(){this.registerEvents()};function approveEventWsData(requestId){return{wsfunction:"tool_dataprivacy_approve_data_request",wsparams:{requestid:requestId}}}function denyEventWsData(requestId){return{wsfunction:"tool_dataprivacy_deny_data_request",wsparams:{requestid:requestId}}}function showConfirmation(action,wsdata){var keys=[];switch(action){case DataPrivacyEvents.approve:keys=[{key:"approverequest",component:"tool_dataprivacy"},{key:"confirmapproval",component:"tool_dataprivacy"}];break;case DataPrivacyEvents.bulkApprove:keys=[{key:"bulkapproverequests",component:"tool_dataprivacy"},{key:"confirmbulkapproval",component:"tool_dataprivacy"}];break;case DataPrivacyEvents.deny:keys=[{key:"denyrequest",component:"tool_dataprivacy"},{key:"confirmdenial",component:"tool_dataprivacy"}];break;case DataPrivacyEvents.bulkDeny:keys=[{key:"bulkdenyrequests",component:"tool_dataprivacy"},{key:"confirmbulkdenial",component:"tool_dataprivacy"}];break;case DataPrivacyEvents.complete:keys=[{key:"markcomplete",component:"tool_dataprivacy"},{key:"confirmcompletion",component:"tool_dataprivacy"}]}var modalTitle="";Str.get_strings(keys).then((function(langStrings){modalTitle=langStrings[0];var confirmMessage=langStrings[1];return ModalFactory.create({title:modalTitle,body:confirmMessage,type:ModalFactory.types.SAVE_CANCEL})})).then((function(modal){modal.setSaveButtonText(modalTitle),modal.getRoot().on(ModalEvents.save,(function(){handleSave(wsdata.wsfunction,wsdata.wsparams)})),modal.getRoot().on(ModalEvents.hidden,(function(){modal.destroy()})),modal.show()})).catch(Notification.exception)}function handleSave(wsfunction,params){var request={methodname:wsfunction,args:params};Ajax.call([request])[0].done((function(data){data.result?window.location.reload():Notification.addNotification({message:data.warnings[0].message,type:"error"})})).fail(Notification.exception)}return RequestActions.prototype.registerEvents=function(){$(ACTIONS_VIEW_REQUEST).click((function(e){e.preventDefault();var requestId=$(this).data("requestid"),request={methodname:"tool_dataprivacy_get_data_request",args:{requestid:requestId}},promises=Ajax.call([request]);$.when(promises[0]).then((function(data){return data.result?data.result:(Notification.addNotification({message:data.warnings[0].message,type:"error"}),!1)})).then((function(data){var body=Templates.render("tool_dataprivacy/request_details",data),templateContext={approvedeny:data.approvedeny,canmarkcomplete:data.canmarkcomplete};return ModalFactory.create({title:data.typename,body:body,type:ModalDataRequest.TYPE,large:!0,templateContext:templateContext})})).then((function(modal){modal.getRoot().on(DataPrivacyEvents.approve,(function(){showConfirmation(DataPrivacyEvents.approve,approveEventWsData(requestId))})),modal.getRoot().on(DataPrivacyEvents.deny,(function(){showConfirmation(DataPrivacyEvents.deny,denyEventWsData(requestId))})),modal.getRoot().on(DataPrivacyEvents.complete,(function(){handleSave("tool_dataprivacy_mark_complete",{requestid:requestId})})),modal.getRoot().on(ModalEvents.hidden,(function(){modal.destroy()})),modal.show()})).catch(Notification.exception)})),$(ACTIONS_APPROVE_REQUEST).click((function(e){e.preventDefault();var requestId=$(this).data("requestid");showConfirmation(DataPrivacyEvents.approve,approveEventWsData(requestId))})),$(ACTIONS_DENY_REQUEST).click((function(e){e.preventDefault();var requestId=$(this).data("requestid");showConfirmation(DataPrivacyEvents.deny,denyEventWsData(requestId))})),$(ACTIONS_MARK_COMPLETE).click((function(e){e.preventDefault();var requestId=$(this).data("requestid");showConfirmation(DataPrivacyEvents.complete,function(requestId){return{wsfunction:"tool_dataprivacy_mark_complete",wsparams:{requestid:requestId}}}(requestId))})),$(ACTIONS_CONFIRM_BULK_ACTION).click((function(){var requestIds=[],actionEvent="",wsdata={},bulkActionKeys=[{key:"selectbulkaction",component:"tool_dataprivacy"},{key:"selectdatarequests",component:"tool_dataprivacy"},{key:"ok"}],bulkaction=parseInt($("#bulk-action").val());if(bulkaction==BULK_ACTIONS_APPROVE||bulkaction==BULK_ACTIONS_DENY)if($(".selectrequests:checked").each((function(){requestIds.push($(this).val())})),requestIds.length<1)Str.get_strings(bulkActionKeys).done((function(langStrings){Notification.alert("",langStrings[1],langStrings[2])})).fail(Notification.exception);else{switch(bulkaction){case BULK_ACTIONS_APPROVE:actionEvent=DataPrivacyEvents.bulkApprove,wsdata=function(requestIds){return{wsfunction:"tool_dataprivacy_bulk_approve_data_requests",wsparams:{requestids:requestIds}}}(requestIds);break;case BULK_ACTIONS_DENY:actionEvent=DataPrivacyEvents.bulkDeny,wsdata=function(requestIds){return{wsfunction:"tool_dataprivacy_bulk_deny_data_requests",wsparams:{requestids:requestIds}}}(requestIds)}showConfirmation(actionEvent,wsdata)}else Str.get_strings(bulkActionKeys).done((function(langStrings){Notification.alert("",langStrings[0],langStrings[2])})).fail(Notification.exception)})),$(ACTIONS_SELECT_ALL).change((function(e){e.preventDefault();var selectAll=$(this).is(":checked");$(SELECTORS_SELECT_REQUEST).prop("checked",selectAll)}))},RequestActions})); //# sourceMappingURL=requestactions.min.js.map amd/build/data_deletion.min.js.map 0000644 00000015057 15152701722 0013106 0 ustar 00 {"version":3,"file":"data_deletion.min.js","sources":["../src/data_deletion.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Request actions.\n *\n * @module tool_dataprivacy/data_deletion\n * @copyright 2018 Jun Pataleta\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([\n 'jquery',\n 'core/ajax',\n 'core/notification',\n 'core/str',\n 'core/modal_factory',\n 'core/modal_events'],\nfunction($, Ajax, Notification, Str, ModalFactory, ModalEvents) {\n\n /**\n * List of action selectors.\n *\n * @type {{MARK_FOR_DELETION: string}}\n * @type {{SELECT_ALL: string}}\n */\n var ACTIONS = {\n MARK_FOR_DELETION: '[data-action=\"markfordeletion\"]',\n SELECT_ALL: '[data-action=\"selectall\"]',\n };\n\n /**\n * List of selectors.\n *\n * @type {{SELECTCONTEXT: string}}\n */\n var SELECTORS = {\n SELECTCONTEXT: '.selectcontext',\n };\n\n /**\n * DataDeletionActions class.\n */\n var DataDeletionActions = function() {\n this.registerEvents();\n };\n\n /**\n * Register event listeners.\n */\n DataDeletionActions.prototype.registerEvents = function() {\n $(ACTIONS.MARK_FOR_DELETION).click(function(e) {\n e.preventDefault();\n\n var selectedIds = [];\n $(SELECTORS.SELECTCONTEXT).each(function() {\n var checkbox = $(this);\n if (checkbox.is(':checked')) {\n selectedIds.push(checkbox.val());\n }\n });\n showConfirmation(selectedIds);\n });\n\n $(ACTIONS.SELECT_ALL).change(function(e) {\n e.preventDefault();\n\n var selectallnone = $(this);\n if (selectallnone.is(':checked')) {\n $(SELECTORS.SELECTCONTEXT).attr('checked', 'checked');\n } else {\n $(SELECTORS.SELECTCONTEXT).removeAttr('checked');\n }\n });\n };\n\n /**\n * Show the confirmation dialogue.\n *\n * @param {Array} ids The array of expired context record IDs.\n */\n function showConfirmation(ids) {\n var keys = [\n {\n key: 'confirm',\n component: 'moodle'\n },\n {\n key: 'confirmcontextdeletion',\n component: 'tool_dataprivacy'\n }\n ];\n var wsfunction = 'tool_dataprivacy_confirm_contexts_for_deletion';\n\n var modalTitle = '';\n Str.get_strings(keys).then(function(langStrings) {\n modalTitle = langStrings[0];\n var confirmMessage = langStrings[1];\n return ModalFactory.create({\n title: modalTitle,\n body: confirmMessage,\n type: ModalFactory.types.SAVE_CANCEL\n });\n }).then(function(modal) {\n modal.setSaveButtonText(modalTitle);\n\n // Handle save event.\n modal.getRoot().on(ModalEvents.save, function() {\n // Confirm the request.\n var params = {\n 'ids': ids\n };\n\n var request = {\n methodname: wsfunction,\n args: params\n };\n\n Ajax.call([request])[0].done(function(data) {\n if (data.result) {\n window.location.reload();\n } else {\n Notification.addNotification({\n message: data.warnings[0].message,\n type: 'error'\n });\n }\n }).fail(Notification.exception);\n });\n\n // Handle hidden event.\n modal.getRoot().on(ModalEvents.hidden, function() {\n // Destroy when hidden.\n modal.destroy();\n });\n\n return modal;\n }).done(function(modal) {\n modal.show();\n }).fail(Notification.exception);\n }\n\n return DataDeletionActions;\n});\n"],"names":["define","$","Ajax","Notification","Str","ModalFactory","ModalEvents","ACTIONS","SELECTORS","DataDeletionActions","registerEvents","prototype","click","e","preventDefault","ids","keys","wsfunction","modalTitle","selectedIds","each","checkbox","this","is","push","val","key","component","get_strings","then","langStrings","confirmMessage","create","title","body","type","types","SAVE_CANCEL","modal","setSaveButtonText","getRoot","on","save","request","methodname","args","call","done","data","result","window","location","reload","addNotification","message","warnings","fail","exception","hidden","destroy","show","change","attr","removeAttr"],"mappings":";;;;;;;AAsBAA,wCAAO,CACH,SACA,YACA,oBACA,WACA,qBACA,sBACJ,SAASC,EAAGC,KAAMC,aAAcC,IAAKC,aAAcC,iBAQ3CC,0BACmB,kCADnBA,mBAEY,4BAQZC,wBACe,iBAMfC,oBAAsB,gBACjBC,yBAMTD,oBAAoBE,UAAUD,eAAiB,WAC3CT,EAAEM,2BAA2BK,OAAM,SAASC,GACxCA,EAAEC,qBA6BgBC,IAClBC,KAUAC,WAEAC,WAxCIC,YAAc,GAClBlB,EAAEO,yBAAyBY,MAAK,eACxBC,SAAWpB,EAAEqB,MACbD,SAASE,GAAG,aACZJ,YAAYK,KAAKH,SAASI,UAuBhBV,IApBDI,YAqBjBH,KAAO,CACP,CACIU,IAAK,UACLC,UAAW,UAEf,CACID,IAAK,yBACLC,UAAW,qBAGfV,WAAa,iDAEbC,WAAa,GACjBd,IAAIwB,YAAYZ,MAAMa,MAAK,SAASC,aAChCZ,WAAaY,YAAY,OACrBC,eAAiBD,YAAY,UAC1BzB,aAAa2B,OAAO,CACvBC,MAAOf,WACPgB,KAAMH,eACNI,KAAM9B,aAAa+B,MAAMC,iBAE9BR,MAAK,SAASS,cACbA,MAAMC,kBAAkBrB,YAGxBoB,MAAME,UAAUC,GAAGnC,YAAYoC,MAAM,eAM7BC,QAAU,CACVC,WAAY3B,WACZ4B,KANS,KACF9B,MAQXb,KAAK4C,KAAK,CAACH,UAAU,GAAGI,MAAK,SAASC,MAC9BA,KAAKC,OACLC,OAAOC,SAASC,SAEhBjD,aAAakD,gBAAgB,CACzBC,QAASN,KAAKO,SAAS,GAAGD,QAC1BnB,KAAM,aAGfqB,KAAKrD,aAAasD,cAIzBnB,MAAME,UAAUC,GAAGnC,YAAYoD,QAAQ,WAEnCpB,MAAMqB,aAGHrB,SACRS,MAAK,SAAST,OACbA,MAAMsB,UACPJ,KAAKrD,aAAasD,cA3ErBxD,EAAEM,oBAAoBsD,QAAO,SAAShD,GAClCA,EAAEC,iBAEkBb,EAAEqB,MACJC,GAAG,YACjBtB,EAAEO,yBAAyBsD,KAAK,UAAW,WAE3C7D,EAAEO,yBAAyBuD,WAAW,eAuE3CtD"} amd/build/effective_retention_period.min.js 0000644 00000002153 15152701722 0015120 0 ustar 00 /** * Module to update the displayed retention period. * * @module tool_dataprivacy/effective_retention_period * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define("tool_dataprivacy/effective_retention_period",["jquery"],(function($){var SELECTORS_PURPOSE_SELECT="#id_purposeid",SELECTORS_RETENTION_FIELD="#fitem_id_retention_current [data-fieldtype=static]",EffectiveRetentionPeriod=function(purposeRetentionPeriods){this.purposeRetentionPeriods=purposeRetentionPeriods,this.registerEventListeners()};return EffectiveRetentionPeriod.prototype.purposeRetentionPeriods=[],EffectiveRetentionPeriod.prototype.registerEventListeners=function(){$(SELECTORS_PURPOSE_SELECT).on("change",function(ev){var selected=$(ev.currentTarget).val(),selectedPurpose=this.purposeRetentionPeriods[selected];$(SELECTORS_RETENTION_FIELD).text(selectedPurpose)}.bind(this))},{init:function(purposeRetentionPeriods){return $(SELECTORS_PURPOSE_SELECT).off("change"),new EffectiveRetentionPeriod(purposeRetentionPeriods)}}})); //# sourceMappingURL=effective_retention_period.min.js.map amd/build/effective_retention_period.min.js.map 0000644 00000007035 15152701722 0015700 0 ustar 00 {"version":3,"file":"effective_retention_period.min.js","sources":["../src/effective_retention_period.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Module to update the displayed retention period.\n *\n * @module tool_dataprivacy/effective_retention_period\n * @copyright 2018 David Monllao\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery'],\n function($) {\n\n var SELECTORS = {\n PURPOSE_SELECT: '#id_purposeid',\n RETENTION_FIELD: '#fitem_id_retention_current [data-fieldtype=static]',\n };\n\n /**\n * Constructor for the retention period display.\n *\n * @param {Array} purposeRetentionPeriods Associative array of purposeids with effective retention period at this context\n */\n var EffectiveRetentionPeriod = function(purposeRetentionPeriods) {\n this.purposeRetentionPeriods = purposeRetentionPeriods;\n this.registerEventListeners();\n };\n\n /**\n * Removes the current 'change' listeners.\n *\n * Useful when a new form is loaded.\n */\n var removeListeners = function() {\n $(SELECTORS.PURPOSE_SELECT).off('change');\n };\n\n /**\n * @var {Array} purposeRetentionPeriods\n * @private\n */\n EffectiveRetentionPeriod.prototype.purposeRetentionPeriods = [];\n\n /**\n * Add purpose change listeners.\n *\n * @method registerEventListeners\n */\n EffectiveRetentionPeriod.prototype.registerEventListeners = function() {\n\n $(SELECTORS.PURPOSE_SELECT).on('change', function(ev) {\n var selected = $(ev.currentTarget).val();\n var selectedPurpose = this.purposeRetentionPeriods[selected];\n $(SELECTORS.RETENTION_FIELD).text(selectedPurpose);\n }.bind(this));\n };\n\n return /** @alias module:tool_dataprivacy/effective_retention_period */ {\n init: function(purposeRetentionPeriods) {\n // Remove previously attached listeners.\n removeListeners();\n return new EffectiveRetentionPeriod(purposeRetentionPeriods);\n }\n };\n }\n);\n\n"],"names":["define","$","SELECTORS","EffectiveRetentionPeriod","purposeRetentionPeriods","registerEventListeners","prototype","on","ev","selected","currentTarget","val","selectedPurpose","this","text","bind","init","off"],"mappings":";;;;;;;AAsBAA,qDAAO,CAAC,WACJ,SAASC,OAEDC,yBACgB,gBADhBA,0BAEiB,sDAQjBC,yBAA2B,SAASC,8BAC/BA,wBAA0BA,6BAC1BC,iCAgBTF,yBAAyBG,UAAUF,wBAA0B,GAO7DD,yBAAyBG,UAAUD,uBAAyB,WAExDJ,EAAEC,0BAA0BK,GAAG,SAAU,SAASC,QAC1CC,SAAWR,EAAEO,GAAGE,eAAeC,MAC/BC,gBAAkBC,KAAKT,wBAAwBK,UACnDR,EAAEC,2BAA2BY,KAAKF,kBACpCG,KAAKF,QAG6D,CACpEG,KAAM,SAASZ,gCAxBfH,EAAEC,0BAA0Be,IAAI,UA2BrB,IAAId,yBAAyBC"} amd/build/defaultsactions.min.js.map 0000644 00000040262 15152701722 0013476 0 ustar 00 {"version":3,"file":"defaultsactions.min.js","sources":["../src/defaultsactions.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * AMD module for data registry defaults actions.\n *\n * @module tool_dataprivacy/defaultsactions\n * @copyright 2018 Jun Pataleta\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([\n 'jquery',\n 'core/ajax',\n 'core/notification',\n 'core/str',\n 'core/modal_factory',\n 'core/modal_events',\n 'core/templates'],\nfunction($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates) {\n\n /**\n * List of action selectors.\n *\n * @type {{EDIT_LEVEL_DEFAULTS: string}}\n * @type {{NEW_ACTIVITY_DEFAULTS: string}}\n * @type {{EDIT_ACTIVITY_DEFAULTS: string}}\n * @type {{DELETE_ACTIVITY_DEFAULTS: string}}\n */\n var ACTIONS = {\n EDIT_LEVEL_DEFAULTS: '[data-action=\"edit-level-defaults\"]',\n NEW_ACTIVITY_DEFAULTS: '[data-action=\"new-activity-defaults\"]',\n EDIT_ACTIVITY_DEFAULTS: '[data-action=\"edit-activity-defaults\"]',\n DELETE_ACTIVITY_DEFAULTS: '[data-action=\"delete-activity-defaults\"]'\n };\n\n /** @type {{INHERIT: Number}} **/\n var INHERIT = -1;\n\n /**\n * DefaultsActions class.\n */\n var DefaultsActions = function() {\n this.registerEvents();\n };\n\n /**\n * Register event listeners.\n */\n DefaultsActions.prototype.registerEvents = function() {\n $(ACTIONS.EDIT_LEVEL_DEFAULTS).click(function(e) {\n e.preventDefault();\n\n var button = $(this);\n var contextLevel = button.data('contextlevel');\n var category = button.data('category');\n var purpose = button.data('purpose');\n\n // Get options.\n var requests = [\n {methodname: 'tool_dataprivacy_get_category_options', args: {}},\n {methodname: 'tool_dataprivacy_get_purpose_options', args: {}}\n ];\n\n var promises = Ajax.call(requests);\n var titlePromise = Str.get_string('editdefaults', 'tool_dataprivacy', $('#defaults-header').text());\n $.when(promises[0], promises[1], titlePromise).then(function(categoryResponse, purposeResponse, title) {\n var categories = categoryResponse.options;\n var purposes = purposeResponse.options;\n showDefaultsFormModal(title, contextLevel, category, purpose, null, categories, purposes, null);\n\n return true;\n }).catch(Notification.exception);\n });\n\n $(ACTIONS.NEW_ACTIVITY_DEFAULTS).click(function(e) {\n e.preventDefault();\n\n var button = $(this);\n var contextLevel = button.data('contextlevel');\n\n // Get options.\n var requests = [\n {methodname: 'tool_dataprivacy_get_category_options', args: {}},\n {methodname: 'tool_dataprivacy_get_purpose_options', args: {}},\n {methodname: 'tool_dataprivacy_get_activity_options', args: {'nodefaults': true}}\n ];\n\n var promises = Ajax.call(requests);\n var titlePromise = Str.get_string('addnewdefaults', 'tool_dataprivacy');\n\n $.when(promises[0], promises[1], promises[2], titlePromise).then(\n function(categoryResponse, purposeResponse, activityResponse, title) {\n var categories = categoryResponse.options;\n var purposes = purposeResponse.options;\n var activities = activityResponse.options;\n\n showDefaultsFormModal(title, contextLevel, null, null, null, categories, purposes, activities);\n\n return true;\n\n }).catch(Notification.exception);\n }\n );\n\n $(ACTIONS.EDIT_ACTIVITY_DEFAULTS).click(function(e) {\n e.preventDefault();\n\n var button = $(this);\n var contextLevel = button.data('contextlevel');\n var category = button.data('category');\n var purpose = button.data('purpose');\n var activity = button.data('activityname');\n\n // Get options.\n var requests = [\n {methodname: 'tool_dataprivacy_get_category_options', args: {}},\n {methodname: 'tool_dataprivacy_get_purpose_options', args: {}},\n {methodname: 'tool_dataprivacy_get_activity_options', args: {}}\n ];\n\n var promises = Ajax.call(requests);\n var titlePromise = Str.get_string('editmoduledefaults', 'tool_dataprivacy');\n\n $.when(promises[0], promises[1], promises[2], titlePromise).then(\n function(categoryResponse, purposeResponse, activityResponse, title) {\n var categories = categoryResponse.options;\n var purposes = purposeResponse.options;\n var activities = activityResponse.options;\n\n showDefaultsFormModal(title, contextLevel, category, purpose, activity, categories, purposes, activities);\n\n return true;\n\n }).catch(Notification.exception);\n }\n );\n\n $(ACTIONS.DELETE_ACTIVITY_DEFAULTS).click(function(e) {\n e.preventDefault();\n\n var button = $(this);\n var contextLevel = button.data('contextlevel');\n var activity = button.data('activityname');\n var activityDisplayName = button.data('activitydisplayname');\n // Set category and purpose to inherit (-1).\n var category = INHERIT;\n var purpose = INHERIT;\n\n ModalFactory.create({\n title: Str.get_string('deletedefaults', 'tool_dataprivacy', activityDisplayName),\n body: Templates.render('tool_dataprivacy/delete_activity_defaults', {\"activityname\": activityDisplayName}),\n type: ModalFactory.types.SAVE_CANCEL,\n large: true\n }).then(function(modal) {\n modal.setSaveButtonText(Str.get_string('delete'));\n\n // Handle save event.\n modal.getRoot().on(ModalEvents.save, function() {\n setContextDefaults(contextLevel, category, purpose, activity, false);\n });\n\n // Handle hidden event.\n modal.getRoot().on(ModalEvents.hidden, function() {\n // Destroy when hidden.\n modal.destroy();\n });\n\n modal.show();\n\n return true;\n }).catch(Notification.exception);\n });\n };\n\n /**\n * Prepares and renders the modal for setting the defaults for the given context level/plugin.\n *\n * @param {String} title The modal's title.\n * @param {Number} contextLevel The context level to set defaults for.\n * @param {Number} category The current category ID.\n * @param {Number} purpose The current purpose ID.\n * @param {String} activity The plugin name of the activity. Optional.\n * @param {Array} categoryOptions The list of category options.\n * @param {Array} purposeOptions The list of purpose options.\n * @param {Array} activityOptions The list of activity options. Optional.\n */\n function showDefaultsFormModal(title, contextLevel, category, purpose, activity,\n categoryOptions, purposeOptions, activityOptions) {\n\n if (category !== null) {\n categoryOptions.forEach(function(currentValue) {\n if (currentValue.id === category) {\n currentValue.selected = true;\n }\n });\n }\n\n if (purpose !== null) {\n purposeOptions.forEach(function(currentValue) {\n if (currentValue.id === purpose) {\n currentValue.selected = true;\n }\n });\n }\n\n var templateContext = {\n \"contextlevel\": contextLevel,\n \"categoryoptions\": categoryOptions,\n \"purposeoptions\": purposeOptions\n };\n\n // Check the activityOptions parameter that was passed.\n if (activityOptions !== null && activityOptions.length) {\n // Check the activity parameter that was passed.\n if (activity === null) {\n // We're setting a new defaults for a module.\n templateContext.newactivitydefaults = true;\n\n } else {\n // Edit mode. Set selection.\n activityOptions.forEach(function(currentValue) {\n if (activity === currentValue.name) {\n currentValue.selected = true;\n }\n });\n }\n\n templateContext.modemodule = true;\n templateContext.activityoptions = activityOptions;\n }\n\n ModalFactory.create({\n title: title,\n body: Templates.render('tool_dataprivacy/category_purpose_form', templateContext),\n type: ModalFactory.types.SAVE_CANCEL,\n large: true\n }).then(function(modal) {\n\n // Handle save event.\n modal.getRoot().on(ModalEvents.save, function() {\n var activity = $('#activity');\n var activityVal = typeof activity !== 'undefined' ? activity.val() : null;\n var override = $('#override');\n var overrideVal = typeof override !== 'undefined' ? override.is(':checked') : false;\n\n setContextDefaults($('#contextlevel').val(), $('#category').val(), $('#purpose').val(), activityVal, overrideVal);\n });\n\n // Handle hidden event.\n modal.getRoot().on(ModalEvents.hidden, function() {\n // Destroy when hidden.\n modal.destroy();\n });\n\n modal.show();\n\n return modal;\n }).catch(Notification.exception);\n }\n\n /**\n * Calls a the tool_dataprivacy_set_context_defaults WS function.\n *\n * @param {Number} contextLevel The context level.\n * @param {Number} category The category ID.\n * @param {Number} purpose The purpose ID.\n * @param {String} activity The plugin name of the activity module.\n * @param {Boolean} override Whether to override custom instances.\n */\n function setContextDefaults(contextLevel, category, purpose, activity, override) {\n var request = {\n methodname: 'tool_dataprivacy_set_context_defaults',\n args: {\n 'contextlevel': contextLevel,\n 'category': category,\n 'purpose': purpose,\n 'override': override,\n 'activity': activity\n }\n };\n\n Ajax.call([request])[0].done(function(data) {\n if (data.result) {\n window.location.reload();\n }\n });\n }\n\n return /** @alias module:tool_dataprivacy/defaultsactions */ {\n // Public variables and functions.\n\n /**\n * Initialise the module.\n *\n * @method init\n * @return {DefaultsActions}\n */\n 'init': function() {\n return new DefaultsActions();\n }\n };\n});\n"],"names":["define","$","Ajax","Notification","Str","ModalFactory","ModalEvents","Templates","ACTIONS","DefaultsActions","registerEvents","showDefaultsFormModal","title","contextLevel","category","purpose","activity","categoryOptions","purposeOptions","activityOptions","forEach","currentValue","id","selected","templateContext","length","newactivitydefaults","name","modemodule","activityoptions","create","body","render","type","types","SAVE_CANCEL","large","then","modal","getRoot","on","save","activityVal","val","override","overrideVal","is","setContextDefaults","hidden","destroy","show","catch","exception","request","methodname","args","call","done","data","result","window","location","reload","prototype","click","e","preventDefault","button","this","promises","titlePromise","get_string","text","when","categoryResponse","purposeResponse","categories","options","purposes","activityResponse","activities","activityDisplayName","setSaveButtonText"],"mappings":";;;;;;;AAsBAA,0CAAO,CACH,SACA,YACA,oBACA,WACA,qBACA,oBACA,mBACJ,SAASC,EAAGC,KAAMC,aAAcC,IAAKC,aAAcC,YAAaC,eAUxDC,4BACqB,sCADrBA,8BAEuB,wCAFvBA,+BAGwB,yCAHxBA,iCAI0B,2CAS1BC,gBAAkB,gBACbC,2BAgJAC,sBAAsBC,MAAOC,aAAcC,SAAUC,QAASC,SACxCC,gBAAiBC,eAAgBC,iBAE3C,OAAbL,UACAG,gBAAgBG,SAAQ,SAASC,cACzBA,aAAaC,KAAOR,WACpBO,aAAaE,UAAW,MAKpB,OAAZR,SACAG,eAAeE,SAAQ,SAASC,cACxBA,aAAaC,KAAOP,UACpBM,aAAaE,UAAW,UAKhCC,gBAAkB,cACFX,6BACGI,+BACDC,gBAIE,OAApBC,iBAA4BA,gBAAgBM,SAE3B,OAAbT,SAEAQ,gBAAgBE,qBAAsB,EAItCP,gBAAgBC,SAAQ,SAASC,cACzBL,WAAaK,aAAaM,OAC1BN,aAAaE,UAAW,MAKpCC,gBAAgBI,YAAa,EAC7BJ,gBAAgBK,gBAAkBV,iBAGtCd,aAAayB,OAAO,CAChBlB,MAAOA,MACPmB,KAAMxB,UAAUyB,OAAO,yCAA0CR,iBACjES,KAAM5B,aAAa6B,MAAMC,YACzBC,OAAO,IACRC,MAAK,SAASC,cAGbA,MAAMC,UAAUC,GAAGlC,YAAYmC,MAAM,eAC7BzB,SAAWf,EAAE,aACbyC,iBAAkC,IAAb1B,SAA2BA,SAAS2B,MAAQ,KACjEC,SAAW3C,EAAE,aACb4C,iBAAkC,IAAbD,UAA2BA,SAASE,GAAG,YAEhEC,mBAAmB9C,EAAE,iBAAiB0C,MAAO1C,EAAE,aAAa0C,MAAO1C,EAAE,YAAY0C,MAAOD,YAAaG,gBAIzGP,MAAMC,UAAUC,GAAGlC,YAAY0C,QAAQ,WAEnCV,MAAMW,aAGVX,MAAMY,OAECZ,SACRa,MAAMhD,aAAaiD,oBAYjBL,mBAAmBlC,aAAcC,SAAUC,QAASC,SAAU4B,cAC/DS,QAAU,CACVC,WAAY,wCACZC,KAAM,cACc1C,sBACJC,iBACDC,iBACC6B,kBACA5B,WAIpBd,KAAKsD,KAAK,CAACH,UAAU,GAAGI,MAAK,SAASC,MAC9BA,KAAKC,QACLC,OAAOC,SAASC,mBA3O5BrD,gBAAgBsD,UAAUrD,eAAiB,WACvCT,EAAEO,6BAA6BwD,OAAM,SAASC,GAC1CA,EAAEC,qBAEEC,OAASlE,EAAEmE,MACXvD,aAAesD,OAAOT,KAAK,gBAC3B5C,SAAWqD,OAAOT,KAAK,YACvB3C,QAAUoD,OAAOT,KAAK,WAQtBW,SAAWnE,KAAKsD,KALL,CACX,CAACF,WAAY,wCAAyCC,KAAM,IAC5D,CAACD,WAAY,uCAAwCC,KAAM,MAI3De,aAAelE,IAAImE,WAAW,eAAgB,mBAAoBtE,EAAE,oBAAoBuE,QAC5FvE,EAAEwE,KAAKJ,SAAS,GAAIA,SAAS,GAAIC,cAAcjC,MAAK,SAASqC,iBAAkBC,gBAAiB/D,WACxFgE,WAAaF,iBAAiBG,QAC9BC,SAAWH,gBAAgBE,eAC/BlE,sBAAsBC,MAAOC,aAAcC,SAAUC,QAAS,KAAM6D,WAAYE,SAAU,OAEnF,KACR3B,MAAMhD,aAAaiD,cAG1BnD,EAAEO,+BAA+BwD,OAAM,SAASC,GAC5CA,EAAEC,qBAGErD,aADSZ,EAAEmE,MACWV,KAAK,gBAS3BW,SAAWnE,KAAKsD,KANL,CACX,CAACF,WAAY,wCAAyCC,KAAM,IAC5D,CAACD,WAAY,uCAAwCC,KAAM,IAC3D,CAACD,WAAY,wCAAyCC,KAAM,aAAe,MAI3Ee,aAAelE,IAAImE,WAAW,iBAAkB,oBAEpDtE,EAAEwE,KAAKJ,SAAS,GAAIA,SAAS,GAAIA,SAAS,GAAIC,cAAcjC,MACxD,SAASqC,iBAAkBC,gBAAiBI,iBAAkBnE,WACtDgE,WAAaF,iBAAiBG,QAC9BC,SAAWH,gBAAgBE,QAC3BG,WAAaD,iBAAiBF,eAElClE,sBAAsBC,MAAOC,aAAc,KAAM,KAAM,KAAM+D,WAAYE,SAAUE,aAE5E,KAER7B,MAAMhD,aAAaiD,cAI9BnD,EAAEO,gCAAgCwD,OAAM,SAASC,GAC7CA,EAAEC,qBAEEC,OAASlE,EAAEmE,MACXvD,aAAesD,OAAOT,KAAK,gBAC3B5C,SAAWqD,OAAOT,KAAK,YACvB3C,QAAUoD,OAAOT,KAAK,WACtB1C,SAAWmD,OAAOT,KAAK,gBASvBW,SAAWnE,KAAKsD,KANL,CACX,CAACF,WAAY,wCAAyCC,KAAM,IAC5D,CAACD,WAAY,uCAAwCC,KAAM,IAC3D,CAACD,WAAY,wCAAyCC,KAAM,MAI5De,aAAelE,IAAImE,WAAW,qBAAsB,oBAExDtE,EAAEwE,KAAKJ,SAAS,GAAIA,SAAS,GAAIA,SAAS,GAAIC,cAAcjC,MACxD,SAASqC,iBAAkBC,gBAAiBI,iBAAkBnE,WACtDgE,WAAaF,iBAAiBG,QAC9BC,SAAWH,gBAAgBE,QAC3BG,WAAaD,iBAAiBF,eAElClE,sBAAsBC,MAAOC,aAAcC,SAAUC,QAASC,SAAU4D,WAAYE,SAAUE,aAEvF,KAER7B,MAAMhD,aAAaiD,cAI9BnD,EAAEO,kCAAkCwD,OAAM,SAASC,GAC/CA,EAAEC,qBAEEC,OAASlE,EAAEmE,MACXvD,aAAesD,OAAOT,KAAK,gBAC3B1C,SAAWmD,OAAOT,KAAK,gBACvBuB,oBAAsBd,OAAOT,KAAK,uBAKtCrD,aAAayB,OAAO,CAChBlB,MAAOR,IAAImE,WAAW,iBAAkB,mBAAoBU,qBAC5DlD,KAAMxB,UAAUyB,OAAO,4CAA6C,cAAiBiD,sBACrFhD,KAAM5B,aAAa6B,MAAMC,YACzBC,OAAO,IACRC,MAAK,SAASC,cACbA,MAAM4C,kBAAkB9E,IAAImE,WAAW,WAGvCjC,MAAMC,UAAUC,GAAGlC,YAAYmC,MAAM,WACjCM,mBAAmBlC,cA1HrB,GAAA,EA0HsDG,UAAU,MAIlEsB,MAAMC,UAAUC,GAAGlC,YAAY0C,QAAQ,WAEnCV,MAAMW,aAGVX,MAAMY,QAEC,KACRC,MAAMhD,aAAaiD,eAsH+B,MASjD,kBACG,IAAI3C"} amd/build/data_registry.min.js 0000644 00000013524 15152701722 0012374 0 ustar 00 /** * Request actions. * * @module tool_dataprivacy/data_registry * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define("tool_dataprivacy/data_registry",["jquery","core/str","core/ajax","core/notification","core/templates","core/modal_factory","core/modal_events","core/fragment","tool_dataprivacy/add_purpose","tool_dataprivacy/add_category"],(function($,Str,Ajax,Notification,Templates,ModalFactory,ModalEvents,Fragment,AddPurpose,AddCategory){var SELECTORS_TREE_NODES="[data-context-tree-node=1]",SELECTORS_FORM_CONTAINER="#context-form-container",DataRegistry=function(systemContextId,initContextLevel,initContextId){this.systemContextId=systemContextId,this.currentContextLevel=initContextLevel,this.currentContextId=initContextId,this.init()};return DataRegistry.prototype.systemContextId=0,DataRegistry.prototype.currentContextLevel=0,DataRegistry.prototype.currentContextId=0,DataRegistry.prototype.addpurpose=null,DataRegistry.prototype.addcategory=null,DataRegistry.prototype.init=function(){this.addpurpose=AddPurpose.getInstance(this.systemContextId),this.addcategory=AddCategory.getInstance(this.systemContextId);this.strings=Str.get_strings([{key:"changessaved",component:"moodle"},{key:"contextpurposecategorysaved",component:"tool_dataprivacy"},{key:"noblockstoload",component:"tool_dataprivacy"},{key:"noactivitiestoload",component:"tool_dataprivacy"},{key:"nocoursestoload",component:"tool_dataprivacy"}]),this.registerEventListeners(),this.currentContextId?this.loadForm("context_form",[this.currentContextId],this.submitContextFormAjax.bind(this)):this.loadForm("contextlevel_form",[this.currentContextLevel],this.submitContextLevelFormAjax.bind(this))},DataRegistry.prototype.registerEventListeners=function(){$(SELECTORS_TREE_NODES).on("click",function(ev){ev.preventDefault();var trigger=$(ev.currentTarget);$(SELECTORS_TREE_NODES).removeClass("active"),trigger.addClass("active");var contextLevel=trigger.data("contextlevel"),contextId=trigger.data("contextid");if(contextLevel)window.history.pushState({},null,"?contextlevel="+contextLevel),this.addpurpose.removeListeners(),this.addcategory.removeListeners(),this.currentContextLevel=contextLevel,this.loadForm("contextlevel_form",[this.currentContextLevel],this.submitContextLevelFormAjax.bind(this));else if(contextId)window.history.pushState({},null,"?contextid="+contextId),this.addpurpose.removeListeners(),this.addcategory.removeListeners(),this.currentContextId=contextId,this.loadForm("context_form",[this.currentContextId],this.submitContextFormAjax.bind(this));else{var expandContextId=trigger.data("expandcontextid"),expandElement=trigger.data("expandelement"),expanded=trigger.data("expanded");expandElement&&(expanded?this.collapse(trigger):!trigger.data("loaded")&&expandContextId&&expandElement?(trigger.find("> i").removeClass("fa-plus"),trigger.find("> i").addClass("fa-circle-o-notch fa-spin"),this.loadExtra(trigger,expandContextId,expandElement)):this.expand(trigger))}}.bind(this))},DataRegistry.prototype.removeListeners=function(){$(SELECTORS_TREE_NODES).off("click")},DataRegistry.prototype.loadForm=function(fragmentName,fragmentArgs,formSubmitCallback){this.clearForm(),Fragment.loadFragment("tool_dataprivacy",fragmentName,this.systemContextId,fragmentArgs).done(function(html,js){$(SELECTORS_FORM_CONTAINER).html(html),Templates.runTemplateJS(js),this.addpurpose.registerEventListeners(),this.addcategory.registerEventListeners(),$(SELECTORS_FORM_CONTAINER).on("submit","form",formSubmitCallback)}.bind(this)).fail(Notification.exception)},DataRegistry.prototype.clearForm=function(){$(SELECTORS_FORM_CONTAINER).off("submit","form")},DataRegistry.prototype.submitForm=function(e){e.preventDefault(),$(SELECTORS_FORM_CONTAINER).find("form").submit()},DataRegistry.prototype.submitContextLevelFormAjax=function(e){this.submitFormAjax(e,"tool_dataprivacy_set_contextlevel_form")},DataRegistry.prototype.submitContextFormAjax=function(e){this.submitFormAjax(e,"tool_dataprivacy_set_context_form")},DataRegistry.prototype.submitFormAjax=function(e,saveMethodName){e.preventDefault();var formData=$(SELECTORS_FORM_CONTAINER).find("form").serialize();return this.strings.then((function(strings){Ajax.call([{methodname:saveMethodName,args:{jsonformdata:JSON.stringify(formData)},done:function(){Notification.alert(strings[0],strings[1])},fail:Notification.exception}])})).catch(Notification.exception)},DataRegistry.prototype.loadExtra=function(parentNode,expandContextId,expandElement){Ajax.call([{methodname:"tool_dataprivacy_tree_extra_branches",args:{contextid:expandContextId,element:expandElement},done:function(data){0!=data.branches.length?Templates.render("tool_dataprivacy/context_tree_branches",data).then(function(html){parentNode.after(html),this.removeListeners(),this.registerEventListeners(),this.expand(parentNode),parentNode.data("loaded",1)}.bind(this)).fail(Notification.exception):this.noElements(parentNode,expandElement)}.bind(this),fail:Notification.exception}])},DataRegistry.prototype.noElements=function(node,expandElement){node.data("expandcontextid",""),node.data("expandelement",""),this.strings.then((function(strings){var key=2;"module"==expandElement?key=3:"course"==expandElement&&(key=4),node.text(strings[key])})).fail(Notification.exception)},DataRegistry.prototype.collapse=function(node){node.data("expanded",0),node.siblings("nav").addClass("hidden"),node.find("> i").removeClass("fa-minus"),node.find("> i").addClass("fa-plus")},DataRegistry.prototype.expand=function(node){node.data("expanded",1),node.siblings("nav").removeClass("hidden"),node.find("> i").removeClass("fa-plus"),node.find("> i").removeClass("fa-circle-o-notch fa-spin"),node.find("> i").addClass("fa-minus")},{init:function(systemContextId,initContextLevel,initContextId){return new DataRegistry(systemContextId,initContextLevel,initContextId)}}})); //# sourceMappingURL=data_registry.min.js.map amd/build/expand_contract.min.js 0000644 00000004151 15152701722 0012703 0 ustar 00 /** * Potential user selector module. * * @module tool_dataprivacy/expand_contract * @copyright 2018 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define("tool_dataprivacy/expand_contract",["jquery","core/url","core/str","core/notification"],(function($,url,str,Notification){var expandedImage=$('<img alt="" src="'+url.imageUrl("t/expanded")+'"/>'),collapsedImage=$('<img alt="" src="'+url.imageUrl("t/collapsed")+'"/>'),CLASSES_EXPAND="fa-caret-right",CLASSES_COLLAPSE="fa-caret-down";return{expandCollapse:function(targetnode,thisnode){targetnode.hasClass("hide")?(targetnode.removeClass("hide"),targetnode.addClass("visible"),targetnode.attr("aria-expanded",!0),thisnode.find(":header i.fa").removeClass(CLASSES_EXPAND),thisnode.find(":header i.fa").addClass(CLASSES_COLLAPSE),thisnode.find(":header img.icon").attr("src",expandedImage.attr("src"))):(targetnode.removeClass("visible"),targetnode.addClass("hide"),targetnode.attr("aria-expanded",!1),thisnode.find(":header i.fa").removeClass(CLASSES_COLLAPSE),thisnode.find(":header i.fa").addClass(CLASSES_EXPAND),thisnode.find(":header img.icon").attr("src",collapsedImage.attr("src")))},expandCollapseAll:function(nextstate){var currentstate="visible"==nextstate?"hide":"visible",ariaexpandedstate="visible"==nextstate,iconclassnow="visible"==nextstate?CLASSES_EXPAND:CLASSES_COLLAPSE,iconclassnext="visible"==nextstate?CLASSES_COLLAPSE:CLASSES_EXPAND,imagenow="visible"==nextstate?expandedImage.attr("src"):collapsedImage.attr("src");$("."+currentstate).each((function(){$(this).removeClass(currentstate),$(this).addClass(nextstate),$(this).attr("aria-expanded",ariaexpandedstate)})),$(".tool_dataprivacy-expand-all").data("visibilityState",currentstate),str.get_string(currentstate,"tool_dataprivacy").then((function(langString){$(".tool_dataprivacy-expand-all").html(langString)})).catch(Notification.exception),$(":header i.fa").each((function(){$(this).removeClass(iconclassnow),$(this).addClass(iconclassnext)})),$(":header img.icon").each((function(){$(this).attr("src",imagenow)}))}}})); //# sourceMappingURL=expand_contract.min.js.map amd/build/contactdpo.min.js.map 0000644 00000006374 15152701722 0012452 0 ustar 00 {"version":3,"file":"contactdpo.min.js","sources":["../src/contactdpo.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Javascript module for contacting the site DPO\n *\n * @module tool_dataprivacy/contactdpo\n * @copyright 2021 Paul Holden <paulh@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport ModalForm from 'core_form/modalform';\nimport Notification from 'core/notification';\nimport {get_string as getString} from 'core/str';\nimport {add as addToast} from 'core/toast';\n\nconst SELECTORS = {\n CONTACT_DPO: '[data-action=\"contactdpo\"]',\n};\n\n/**\n * Initialize module\n */\nexport const init = () => {\n const triggerElement = document.querySelector(SELECTORS.CONTACT_DPO);\n\n triggerElement.addEventListener('click', event => {\n event.preventDefault();\n\n const modalForm = new ModalForm({\n modalConfig: {\n title: getString('contactdataprotectionofficer', 'tool_dataprivacy'),\n },\n formClass: 'tool_dataprivacy\\\\form\\\\contactdpo',\n saveButtonText: getString('send', 'tool_dataprivacy'),\n returnFocus: triggerElement,\n });\n\n // Show a toast notification when the form is submitted.\n modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, event => {\n if (event.detail.result) {\n getString('requestsubmitted', 'tool_dataprivacy').then(addToast).catch();\n } else {\n const warningMessages = event.detail.warnings.map(warning => warning.message);\n Notification.addNotification({\n type: 'error',\n message: warningMessages.join('<br>')\n });\n }\n });\n\n modalForm.show();\n });\n};\n"],"names":["SELECTORS","triggerElement","document","querySelector","addEventListener","event","preventDefault","modalForm","ModalForm","modalConfig","title","formClass","saveButtonText","returnFocus","events","FORM_SUBMITTED","detail","result","then","addToast","catch","warningMessages","warnings","map","warning","message","addNotification","type","join","show"],"mappings":";;;;;;;0LA4BMA,sBACW,2CAMG,WACVC,eAAiBC,SAASC,cAAcH,uBAE9CC,eAAeG,iBAAiB,SAASC,QACrCA,MAAMC,uBAEAC,UAAY,IAAIC,mBAAU,CAC5BC,YAAa,CACTC,OAAO,mBAAU,+BAAgC,qBAErDC,UAAW,qCACXC,gBAAgB,mBAAU,OAAQ,oBAClCC,YAAaZ,iBAIjBM,UAAUH,iBAAiBG,UAAUO,OAAOC,gBAAgBV,WACpDA,MAAMW,OAAOC,2BACH,mBAAoB,oBAAoBC,KAAKC,YAAUC,YAC9D,OACGC,gBAAkBhB,MAAMW,OAAOM,SAASC,KAAIC,SAAWA,QAAQC,gCACxDC,gBAAgB,CACzBC,KAAM,QACNF,QAASJ,gBAAgBO,KAAK,cAK1CrB,UAAUsB"} amd/build/myrequestactions.min.js.map 0000644 00000007265 15152701722 0013733 0 ustar 00 {"version":3,"file":"myrequestactions.min.js","sources":["../src/myrequestactions.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * AMD module to enable users to manage their own data requests.\n *\n * @module tool_dataprivacy/myrequestactions\n * @copyright 2018 Jun Pataleta\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\nimport {get_strings as getStrings} from 'core/str';\n\nconst SELECTORS = {\n CANCEL_REQUEST: '[data-action=\"cancel\"][data-requestid]',\n};\n\n/**\n * Initialize module\n */\nexport const init = () => {\n document.addEventListener('click', event => {\n const triggerElement = event.target.closest(SELECTORS.CANCEL_REQUEST);\n if (triggerElement === null) {\n return;\n }\n\n event.preventDefault();\n\n const requiredStrings = [\n {key: 'cancelrequest', component: 'tool_dataprivacy'},\n {key: 'cancelrequestconfirmation', component: 'tool_dataprivacy'},\n ];\n\n getStrings(requiredStrings).then(([cancelRequest, cancelConfirm]) => {\n return Notification.confirm(cancelRequest, cancelConfirm, cancelRequest, null, () => {\n const pendingPromise = new Pending('tool/dataprivacy:cancelRequest');\n const request = {\n methodname: 'tool_dataprivacy_cancel_data_request',\n args: {requestid: triggerElement.dataset.requestid}\n };\n\n Ajax.call([request])[0].then(response => {\n if (response.result) {\n window.location.reload();\n } else {\n Notification.addNotification({\n type: 'error',\n message: response.warnings[0].message\n });\n }\n return pendingPromise.resolve();\n }).catch(Notification.exception);\n });\n }).catch();\n });\n};\n"],"names":["SELECTORS","document","addEventListener","event","triggerElement","target","closest","preventDefault","key","component","then","_ref","cancelRequest","cancelConfirm","Notification","confirm","pendingPromise","Pending","request","methodname","args","requestid","dataset","call","response","result","window","location","reload","addNotification","type","message","warnings","resolve","catch","exception"],"mappings":";;;;;;;0NA4BMA,yBACc,uDAMA,KAChBC,SAASC,iBAAiB,SAASC,cACzBC,eAAiBD,MAAME,OAAOC,QAAQN,6BACrB,OAAnBI,sBAIJD,MAAMI,sCAEkB,CACpB,CAACC,IAAK,gBAAiBC,UAAW,oBAClC,CAACD,IAAK,4BAA6BC,UAAW,sBAGtBC,MAAKC,WAAEC,cAAeC,2BACvCC,sBAAaC,QAAQH,cAAeC,cAAeD,cAAe,MAAM,WACrEI,eAAiB,IAAIC,iBAAQ,kCAC7BC,QAAU,CACZC,WAAY,uCACZC,KAAM,CAACC,UAAWjB,eAAekB,QAAQD,0BAGxCE,KAAK,CAACL,UAAU,GAAGR,MAAKc,WACrBA,SAASC,OACTC,OAAOC,SAASC,+BAEHC,gBAAgB,CACzBC,KAAM,QACNC,QAASP,SAASQ,SAAS,GAAGD,UAG/Bf,eAAeiB,aACvBC,MAAMpB,sBAAaqB,iBAE3BD"} amd/build/events.min.js 0000644 00000001054 15152701722 0011032 0 ustar 00 /** * Contain the events the data privacy tool can fire. * * @module tool_dataprivacy/events * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define("tool_dataprivacy/events",[],(function(){return{approve:"tool_dataprivacy-data_request:approve",bulkApprove:"tool_dataprivacy-data_request:bulk_approve",deny:"tool_dataprivacy-data_request:deny",bulkDeny:"tool_dataprivacy-data_request:bulk_deny",complete:"tool_dataprivacy-data_request:complete"}})); //# sourceMappingURL=events.min.js.map amd/build/events.min.js.map 0000644 00000002775 15152701722 0011621 0 ustar 00 {"version":3,"file":"events.min.js","sources":["../src/events.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Contain the events the data privacy tool can fire.\n *\n * @module tool_dataprivacy/events\n * @copyright 2018 Jun Pataleta\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([], function() {\n return {\n approve: 'tool_dataprivacy-data_request:approve',\n bulkApprove: 'tool_dataprivacy-data_request:bulk_approve',\n deny: 'tool_dataprivacy-data_request:deny',\n bulkDeny: 'tool_dataprivacy-data_request:bulk_deny',\n complete: 'tool_dataprivacy-data_request:complete'\n };\n});\n"],"names":["define","approve","bulkApprove","deny","bulkDeny","complete"],"mappings":";;;;;;;AAsBAA,iCAAO,IAAI,iBACA,CACHC,QAAS,wCACTC,YAAa,6CACbC,KAAM,qCACNC,SAAU,0CACVC,SAAU"} amd/build/form-user-selector.min.js.map 0000644 00000006631 15152701722 0014045 0 ustar 00 {"version":3,"file":"form-user-selector.min.js","sources":["../src/form-user-selector.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Potential user selector module.\n *\n * @module tool_dataprivacy/form-user-selector\n * @copyright 2018 Jun Pataleta\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery', 'core/ajax', 'core/templates'], function($, Ajax, Templates) {\n\n return /** @alias module:tool_dataprivacy/form-user-selector */ {\n\n processResults: function(selector, results) {\n var users = [];\n $.each(results, function(index, user) {\n users.push({\n value: user.id,\n label: user._label\n });\n });\n return users;\n },\n\n transport: function(selector, query, success, failure) {\n var promise;\n\n promise = Ajax.call([{\n methodname: 'tool_dataprivacy_get_users',\n args: {\n query: query\n }\n }]);\n\n promise[0].then(function(results) {\n var promises = [],\n i = 0;\n\n // Render the label.\n $.each(results, function(index, user) {\n promises.push(Templates.render('tool_dataprivacy/form-user-selector-suggestion', user));\n });\n\n // Apply the label to the results.\n return $.when.apply($.when, promises).then(function() {\n var args = arguments;\n $.each(results, function(index, user) {\n user._label = args[i];\n i++;\n });\n success(results);\n return;\n });\n\n }).fail(failure);\n }\n\n };\n\n});\n"],"names":["define","$","Ajax","Templates","processResults","selector","results","users","each","index","user","push","value","id","label","_label","transport","query","success","failure","call","methodname","args","then","promises","i","render","when","apply","arguments","fail"],"mappings":";;;;;;;AAuBAA,6CAAO,CAAC,SAAU,YAAa,mBAAmB,SAASC,EAAGC,KAAMC,iBAEA,CAE5DC,eAAgB,SAASC,SAAUC,aAC3BC,MAAQ,UACZN,EAAEO,KAAKF,SAAS,SAASG,MAAOC,MAC5BH,MAAMI,KAAK,CACPC,MAAOF,KAAKG,GACZC,MAAOJ,KAAKK,YAGbR,OAGXS,UAAW,SAASX,SAAUY,MAAOC,QAASC,SAGhCjB,KAAKkB,KAAK,CAAC,CACjBC,WAAY,6BACZC,KAAM,CACFL,MAAOA,UAIP,GAAGM,MAAK,SAASjB,aACjBkB,SAAW,GACXC,EAAI,SAGRxB,EAAEO,KAAKF,SAAS,SAASG,MAAOC,MAC5Bc,SAASb,KAAKR,UAAUuB,OAAO,iDAAkDhB,UAI9ET,EAAE0B,KAAKC,MAAM3B,EAAE0B,KAAMH,UAAUD,MAAK,eACnCD,KAAOO,UACX5B,EAAEO,KAAKF,SAAS,SAASG,MAAOC,MAC5BA,KAAKK,OAASO,KAAKG,GACnBA,OAEJP,QAAQZ,eAIbwB,KAAKX"} amd/build/categoriesactions.min.js 0000644 00000003246 15152701722 0013241 0 ustar 00 /** * AMD module for categories actions. * * @module tool_dataprivacy/categoriesactions * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define("tool_dataprivacy/categoriesactions",["jquery","core/ajax","core/notification","core/str","core/modal_factory","core/modal_events"],(function($,Ajax,Notification,Str,ModalFactory,ModalEvents){var ACTIONS_DELETE='[data-action="deletecategory"]',CategoriesActions=function(){this.registerEvents()};return CategoriesActions.prototype.registerEvents=function(){$(ACTIONS_DELETE).click((function(e){e.preventDefault();var id=$(this).data("id"),stringkeys=[{key:"deletecategory",component:"tool_dataprivacy"},{key:"deletecategorytext",component:"tool_dataprivacy",param:$(this).data("name")},{key:"delete"}];Str.get_strings(stringkeys).then((function(langStrings){var title=langStrings[0],confirmMessage=langStrings[1],buttonText=langStrings[2];return ModalFactory.create({title:title,body:confirmMessage,type:ModalFactory.types.SAVE_CANCEL}).then((function(modal){return modal.setSaveButtonText(buttonText),modal.getRoot().on(ModalEvents.save,(function(){var request={methodname:"tool_dataprivacy_delete_category",args:{id:id}};Ajax.call([request])[0].done((function(data){data.result?$('tr[data-categoryid="'+id+'"]').remove():Notification.addNotification({message:data.warnings[0].message,type:"error"})})).fail(Notification.exception)})),modal.getRoot().on(ModalEvents.hidden,(function(){modal.destroy()})),modal}))})).done((function(modal){modal.show()})).fail(Notification.exception)}))},{init:function(){return new CategoriesActions}}})); //# sourceMappingURL=categoriesactions.min.js.map amd/build/request_filter.min.js 0000644 00000002035 15152701722 0012563 0 ustar 00 /** * JS module for the data requests filter. * * @module tool_dataprivacy/request_filter * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define("tool_dataprivacy/request_filter",["jquery","core/form-autocomplete","core/str","core/notification"],(function($,Autocomplete,Str,Notification){var SELECTORS_REQUEST_FILTERS="#request-filters";return{init:function(){!function(){Str.get_strings([{key:"filter",component:"moodle"},{key:"nofiltersapplied",component:"moodle"}]).then((function(langstrings){var placeholder=langstrings[0],noSelectionString=langstrings[1];return Autocomplete.enhance(SELECTORS_REQUEST_FILTERS,!1,"",placeholder,!1,!0,noSelectionString,!0)})).fail(Notification.exception);var last=$(SELECTORS_REQUEST_FILTERS).val();$(SELECTORS_REQUEST_FILTERS).on("change",(function(){var current=$(this).val();last.join(",")!==current.join(",")&&(0===current.length&&$("#filters-cleared").val(1),$(this.form).submit())}))}()}}})); //# sourceMappingURL=request_filter.min.js.map amd/build/purposesactions.min.js.map 0000644 00000013443 15152701722 0013550 0 ustar 00 {"version":3,"file":"purposesactions.min.js","sources":["../src/purposesactions.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * AMD module for purposes actions.\n *\n * @module tool_dataprivacy/purposesactions\n * @copyright 2018 David Monllao\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([\n 'jquery',\n 'core/ajax',\n 'core/notification',\n 'core/str',\n 'core/modal_factory',\n 'core/modal_events'],\nfunction($, Ajax, Notification, Str, ModalFactory, ModalEvents) {\n\n /**\n * List of action selectors.\n *\n * @type {{DELETE: string}}\n */\n var ACTIONS = {\n DELETE: '[data-action=\"deletepurpose\"]',\n };\n\n /**\n * PurposesActions class.\n */\n var PurposesActions = function() {\n this.registerEvents();\n };\n\n /**\n * Register event listeners.\n */\n PurposesActions.prototype.registerEvents = function() {\n $(ACTIONS.DELETE).click(function(e) {\n e.preventDefault();\n\n var id = $(this).data('id');\n var purposename = $(this).data('name');\n var stringkeys = [\n {\n key: 'deletepurpose',\n component: 'tool_dataprivacy'\n },\n {\n key: 'deletepurposetext',\n component: 'tool_dataprivacy',\n param: purposename\n },\n {\n key: 'delete'\n }\n ];\n\n Str.get_strings(stringkeys).then(function(langStrings) {\n var title = langStrings[0];\n var confirmMessage = langStrings[1];\n var buttonText = langStrings[2];\n return ModalFactory.create({\n title: title,\n body: confirmMessage,\n type: ModalFactory.types.SAVE_CANCEL\n }).then(function(modal) {\n modal.setSaveButtonText(buttonText);\n\n // Handle save event.\n modal.getRoot().on(ModalEvents.save, function() {\n\n var request = {\n methodname: 'tool_dataprivacy_delete_purpose',\n args: {'id': id}\n };\n\n Ajax.call([request])[0].done(function(data) {\n if (data.result) {\n $('tr[data-purposeid=\"' + id + '\"]').remove();\n } else {\n Notification.addNotification({\n message: data.warnings[0].message,\n type: 'error'\n });\n }\n }).fail(Notification.exception);\n });\n\n // Handle hidden event.\n modal.getRoot().on(ModalEvents.hidden, function() {\n // Destroy when hidden.\n modal.destroy();\n });\n\n return modal;\n });\n }).done(function(modal) {\n modal.show();\n\n }).fail(Notification.exception);\n });\n };\n\n return /** @alias module:tool_dataprivacy/purposesactions */ {\n // Public variables and functions.\n\n /**\n * Initialise the module.\n *\n * @method init\n * @return {PurposesActions}\n */\n 'init': function() {\n return new PurposesActions();\n }\n };\n});\n"],"names":["define","$","Ajax","Notification","Str","ModalFactory","ModalEvents","ACTIONS","PurposesActions","registerEvents","prototype","click","e","preventDefault","id","this","data","stringkeys","key","component","param","get_strings","then","langStrings","title","confirmMessage","buttonText","create","body","type","types","SAVE_CANCEL","modal","setSaveButtonText","getRoot","on","save","request","methodname","args","call","done","result","remove","addNotification","message","warnings","fail","exception","hidden","destroy","show"],"mappings":";;;;;;;AAsBAA,0CAAO,CACH,SACA,YACA,oBACA,WACA,qBACA,sBACJ,SAASC,EAAGC,KAAMC,aAAcC,IAAKC,aAAcC,iBAO3CC,eACQ,gCAMRC,gBAAkB,gBACbC,yBAMTD,gBAAgBE,UAAUD,eAAiB,WACvCR,EAAEM,gBAAgBI,OAAM,SAASC,GAC7BA,EAAEC,qBAEEC,GAAKb,EAAEc,MAAMC,KAAK,MAElBC,WAAa,CACb,CACIC,IAAK,gBACLC,UAAW,oBAEf,CACID,IAAK,oBACLC,UAAW,mBACXC,MATUnB,EAAEc,MAAMC,KAAK,SAW3B,CACIE,IAAK,WAIbd,IAAIiB,YAAYJ,YAAYK,MAAK,SAASC,iBAClCC,MAAQD,YAAY,GACpBE,eAAiBF,YAAY,GAC7BG,WAAaH,YAAY,UACtBlB,aAAasB,OAAO,CACvBH,MAAOA,MACPI,KAAMH,eACNI,KAAMxB,aAAayB,MAAMC,cAC1BT,MAAK,SAASU,cACbA,MAAMC,kBAAkBP,YAGxBM,MAAME,UAAUC,GAAG7B,YAAY8B,MAAM,eAE7BC,QAAU,CACVC,WAAY,kCACZC,KAAM,IAAOzB,KAGjBZ,KAAKsC,KAAK,CAACH,UAAU,GAAGI,MAAK,SAASzB,MAC9BA,KAAK0B,OACLzC,EAAE,sBAAwBa,GAAK,MAAM6B,SAErCxC,aAAayC,gBAAgB,CACzBC,QAAS7B,KAAK8B,SAAS,GAAGD,QAC1BhB,KAAM,aAGfkB,KAAK5C,aAAa6C,cAIzBhB,MAAME,UAAUC,GAAG7B,YAAY2C,QAAQ,WAEnCjB,MAAMkB,aAGHlB,YAEZS,MAAK,SAAST,OACbA,MAAMmB,UAEPJ,KAAK5C,aAAa6C,eAIgC,MASjD,kBACG,IAAIxC"} amd/build/data_registry.min.js.map 0000644 00000041507 15152701722 0013152 0 ustar 00 {"version":3,"file":"data_registry.min.js","sources":["../src/data_registry.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Request actions.\n *\n * @module tool_dataprivacy/data_registry\n * @copyright 2018 David Monllao\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery', 'core/str', 'core/ajax', 'core/notification', 'core/templates', 'core/modal_factory',\n 'core/modal_events', 'core/fragment', 'tool_dataprivacy/add_purpose', 'tool_dataprivacy/add_category'],\n function($, Str, Ajax, Notification, Templates, ModalFactory, ModalEvents, Fragment, AddPurpose, AddCategory) {\n\n var SELECTORS = {\n TREE_NODES: '[data-context-tree-node=1]',\n FORM_CONTAINER: '#context-form-container',\n };\n\n var DataRegistry = function(systemContextId, initContextLevel, initContextId) {\n this.systemContextId = systemContextId;\n this.currentContextLevel = initContextLevel;\n this.currentContextId = initContextId;\n this.init();\n };\n\n /**\n * @var {int} systemContextId\n * @private\n */\n DataRegistry.prototype.systemContextId = 0;\n\n /**\n * @var {int} currentContextLevel\n * @private\n */\n DataRegistry.prototype.currentContextLevel = 0;\n\n /**\n * @var {int} currentContextId\n * @private\n */\n DataRegistry.prototype.currentContextId = 0;\n\n /**\n * @var {AddPurpose} addpurpose\n * @private\n */\n DataRegistry.prototype.addpurpose = null;\n\n /**\n * @var {AddCategory} addcategory\n * @private\n */\n DataRegistry.prototype.addcategory = null;\n\n DataRegistry.prototype.init = function() {\n // Add purpose and category modals always at system context.\n this.addpurpose = AddPurpose.getInstance(this.systemContextId);\n this.addcategory = AddCategory.getInstance(this.systemContextId);\n\n var stringKeys = [\n {\n key: 'changessaved',\n component: 'moodle'\n }, {\n key: 'contextpurposecategorysaved',\n component: 'tool_dataprivacy'\n }, {\n key: 'noblockstoload',\n component: 'tool_dataprivacy'\n }, {\n key: 'noactivitiestoload',\n component: 'tool_dataprivacy'\n }, {\n key: 'nocoursestoload',\n component: 'tool_dataprivacy'\n }\n ];\n this.strings = Str.get_strings(stringKeys);\n\n this.registerEventListeners();\n\n // Load the default context level form.\n if (this.currentContextId) {\n this.loadForm('context_form', [this.currentContextId], this.submitContextFormAjax.bind(this));\n } else {\n this.loadForm('contextlevel_form', [this.currentContextLevel], this.submitContextLevelFormAjax.bind(this));\n }\n };\n\n DataRegistry.prototype.registerEventListeners = function() {\n $(SELECTORS.TREE_NODES).on('click', function(ev) {\n ev.preventDefault();\n\n var trigger = $(ev.currentTarget);\n\n // Active node.\n $(SELECTORS.TREE_NODES).removeClass('active');\n trigger.addClass('active');\n\n var contextLevel = trigger.data('contextlevel');\n var contextId = trigger.data('contextid');\n if (contextLevel) {\n // Context level level.\n\n window.history.pushState({}, null, '?contextlevel=' + contextLevel);\n\n // Remove previous add purpose and category listeners to avoid memory leaks.\n this.addpurpose.removeListeners();\n this.addcategory.removeListeners();\n\n // Load the context level form.\n this.currentContextLevel = contextLevel;\n this.loadForm('contextlevel_form', [this.currentContextLevel], this.submitContextLevelFormAjax.bind(this));\n } else if (contextId) {\n // Context instance level.\n\n window.history.pushState({}, null, '?contextid=' + contextId);\n\n // Remove previous add purpose and category listeners to avoid memory leaks.\n this.addpurpose.removeListeners();\n this.addcategory.removeListeners();\n\n // Load the context level form.\n this.currentContextId = contextId;\n this.loadForm('context_form', [this.currentContextId], this.submitContextFormAjax.bind(this));\n } else {\n // Expandable nodes.\n\n var expandContextId = trigger.data('expandcontextid');\n var expandElement = trigger.data('expandelement');\n var expanded = trigger.data('expanded');\n\n // Extra checking that there is an expandElement because we remove it after loading 0 branches.\n if (expandElement) {\n\n if (!expanded) {\n if (trigger.data('loaded') || !expandContextId || !expandElement) {\n this.expand(trigger);\n } else {\n\n trigger.find('> i').removeClass('fa-plus');\n trigger.find('> i').addClass('fa-circle-o-notch fa-spin');\n this.loadExtra(trigger, expandContextId, expandElement);\n }\n } else {\n this.collapse(trigger);\n }\n }\n }\n\n }.bind(this));\n };\n\n DataRegistry.prototype.removeListeners = function() {\n $(SELECTORS.TREE_NODES).off('click');\n };\n\n DataRegistry.prototype.loadForm = function(fragmentName, fragmentArgs, formSubmitCallback) {\n\n this.clearForm();\n\n var fragment = Fragment.loadFragment('tool_dataprivacy', fragmentName, this.systemContextId, fragmentArgs);\n fragment.done(function(html, js) {\n\n $(SELECTORS.FORM_CONTAINER).html(html);\n Templates.runTemplateJS(js);\n\n this.addpurpose.registerEventListeners();\n this.addcategory.registerEventListeners();\n\n // We also catch the form submit event and use it to submit the form with ajax.\n $(SELECTORS.FORM_CONTAINER).on('submit', 'form', formSubmitCallback);\n\n }.bind(this)).fail(Notification.exception);\n };\n\n DataRegistry.prototype.clearForm = function() {\n // Remove previous listeners.\n $(SELECTORS.FORM_CONTAINER).off('submit', 'form');\n };\n\n /**\n * This triggers a form submission, so that any mform elements can do final tricks before the form submission is processed.\n *\n * @method submitForm\n * @param {Event} e Form submission event.\n * @private\n */\n DataRegistry.prototype.submitForm = function(e) {\n e.preventDefault();\n $(SELECTORS.FORM_CONTAINER).find('form').submit();\n };\n\n DataRegistry.prototype.submitContextLevelFormAjax = function(e) {\n this.submitFormAjax(e, 'tool_dataprivacy_set_contextlevel_form');\n };\n\n DataRegistry.prototype.submitContextFormAjax = function(e) {\n this.submitFormAjax(e, 'tool_dataprivacy_set_context_form');\n };\n\n DataRegistry.prototype.submitFormAjax = function(e, saveMethodName) {\n // We don't want to do a real form submission.\n e.preventDefault();\n\n // Convert all the form elements values to a serialised string.\n var formData = $(SELECTORS.FORM_CONTAINER).find('form').serialize();\n return this.strings.then(function(strings) {\n Ajax.call([{\n methodname: saveMethodName,\n args: {jsonformdata: JSON.stringify(formData)},\n done: function() {\n Notification.alert(strings[0], strings[1]);\n },\n fail: Notification.exception\n }]);\n return;\n }).catch(Notification.exception);\n\n };\n\n DataRegistry.prototype.loadExtra = function(parentNode, expandContextId, expandElement) {\n\n Ajax.call([{\n methodname: 'tool_dataprivacy_tree_extra_branches',\n args: {\n contextid: expandContextId,\n element: expandElement,\n },\n done: function(data) {\n if (data.branches.length == 0) {\n this.noElements(parentNode, expandElement);\n return;\n }\n Templates.render('tool_dataprivacy/context_tree_branches', data)\n .then(function(html) {\n parentNode.after(html);\n this.removeListeners();\n this.registerEventListeners();\n this.expand(parentNode);\n parentNode.data('loaded', 1);\n return;\n }.bind(this))\n .fail(Notification.exception);\n }.bind(this),\n fail: Notification.exception\n }]);\n };\n\n DataRegistry.prototype.noElements = function(node, expandElement) {\n node.data('expandcontextid', '');\n node.data('expandelement', '');\n this.strings.then(function(strings) {\n\n // 2 = blocks, 3 = activities, 4 = courses (although courses is not likely really).\n var key = 2;\n if (expandElement == 'module') {\n key = 3;\n } else if (expandElement == 'course') {\n key = 4;\n }\n node.text(strings[key]);\n return;\n }).fail(Notification.exception);\n };\n\n DataRegistry.prototype.collapse = function(node) {\n node.data('expanded', 0);\n node.siblings('nav').addClass('hidden');\n node.find('> i').removeClass('fa-minus');\n node.find('> i').addClass('fa-plus');\n };\n\n DataRegistry.prototype.expand = function(node) {\n node.data('expanded', 1);\n node.siblings('nav').removeClass('hidden');\n node.find('> i').removeClass('fa-plus');\n // Also remove the spinning one if data was just loaded.\n node.find('> i').removeClass('fa-circle-o-notch fa-spin');\n node.find('> i').addClass('fa-minus');\n };\n return /** @alias module:tool_dataprivacy/data_registry */ {\n\n /**\n * Initialise the page.\n *\n * @param {Number} systemContextId\n * @param {Number} initContextLevel\n * @param {Number} initContextId\n * @return {DataRegistry}\n */\n init: function(systemContextId, initContextLevel, initContextId) {\n return new DataRegistry(systemContextId, initContextLevel, initContextId);\n }\n };\n }\n);\n\n"],"names":["define","$","Str","Ajax","Notification","Templates","ModalFactory","ModalEvents","Fragment","AddPurpose","AddCategory","SELECTORS","DataRegistry","systemContextId","initContextLevel","initContextId","currentContextLevel","currentContextId","init","prototype","addpurpose","addcategory","getInstance","this","strings","get_strings","key","component","registerEventListeners","loadForm","submitContextFormAjax","bind","submitContextLevelFormAjax","on","ev","preventDefault","trigger","currentTarget","removeClass","addClass","contextLevel","data","contextId","window","history","pushState","removeListeners","expandContextId","expandElement","expanded","collapse","find","loadExtra","expand","off","fragmentName","fragmentArgs","formSubmitCallback","clearForm","loadFragment","done","html","js","runTemplateJS","fail","exception","submitForm","e","submit","submitFormAjax","saveMethodName","formData","serialize","then","call","methodname","args","jsonformdata","JSON","stringify","alert","catch","parentNode","contextid","element","branches","length","render","after","noElements","node","text","siblings"],"mappings":";;;;;;;AAsBAA,wCAAO,CAAC,SAAU,WAAY,YAAa,oBAAqB,iBAAkB,qBAC9E,oBAAqB,gBAAiB,+BAAgC,kCACtE,SAASC,EAAGC,IAAKC,KAAMC,aAAcC,UAAWC,aAAcC,YAAaC,SAAUC,WAAYC,iBAEzFC,qBACY,6BADZA,yBAEgB,0BAGhBC,aAAe,SAASC,gBAAiBC,iBAAkBC,oBACtDF,gBAAkBA,qBAClBG,oBAAsBF,sBACtBG,iBAAmBF,mBACnBG,eAOTN,aAAaO,UAAUN,gBAAkB,EAMzCD,aAAaO,UAAUH,oBAAsB,EAM7CJ,aAAaO,UAAUF,iBAAmB,EAM1CL,aAAaO,UAAUC,WAAa,KAMpCR,aAAaO,UAAUE,YAAc,KAErCT,aAAaO,UAAUD,KAAO,gBAErBE,WAAaX,WAAWa,YAAYC,KAAKV,sBACzCQ,YAAcX,YAAYY,YAAYC,KAAKV,sBAoB3CW,QAAUtB,IAAIuB,YAlBF,CACb,CACIC,IAAK,eACLC,UAAW,UACZ,CACCD,IAAK,8BACLC,UAAW,oBACZ,CACCD,IAAK,iBACLC,UAAW,oBACZ,CACCD,IAAK,qBACLC,UAAW,oBACZ,CACCD,IAAK,kBACLC,UAAW,2BAKdC,yBAGDL,KAAKN,sBACAY,SAAS,eAAgB,CAACN,KAAKN,kBAAmBM,KAAKO,sBAAsBC,KAAKR,YAElFM,SAAS,oBAAqB,CAACN,KAAKP,qBAAsBO,KAAKS,2BAA2BD,KAAKR,QAI5GX,aAAaO,UAAUS,uBAAyB,WAC5C3B,EAAEU,sBAAsBsB,GAAG,QAAS,SAASC,IACzCA,GAAGC,qBAECC,QAAUnC,EAAEiC,GAAGG,eAGnBpC,EAAEU,sBAAsB2B,YAAY,UACpCF,QAAQG,SAAS,cAEbC,aAAeJ,QAAQK,KAAK,gBAC5BC,UAAYN,QAAQK,KAAK,gBACzBD,aAGAG,OAAOC,QAAQC,UAAU,GAAI,KAAM,iBAAmBL,mBAGjDpB,WAAW0B,uBACXzB,YAAYyB,uBAGZ9B,oBAAsBwB,kBACtBX,SAAS,oBAAqB,CAACN,KAAKP,qBAAsBO,KAAKS,2BAA2BD,KAAKR,YACjG,GAAImB,UAGPC,OAAOC,QAAQC,UAAU,GAAI,KAAM,cAAgBH,gBAG9CtB,WAAW0B,uBACXzB,YAAYyB,uBAGZ7B,iBAAmByB,eACnBb,SAAS,eAAgB,CAACN,KAAKN,kBAAmBM,KAAKO,sBAAsBC,KAAKR,WACpF,KAGCwB,gBAAkBX,QAAQK,KAAK,mBAC/BO,cAAgBZ,QAAQK,KAAK,iBAC7BQ,SAAWb,QAAQK,KAAK,YAGxBO,gBAEKC,cAUIC,SAASd,UATVA,QAAQK,KAAK,WAAcM,iBAAoBC,eAI/CZ,QAAQe,KAAK,OAAOb,YAAY,WAChCF,QAAQe,KAAK,OAAOZ,SAAS,kCACxBa,UAAUhB,QAASW,gBAAiBC,qBALpCK,OAAOjB,YAa9BL,KAAKR,QAGXX,aAAaO,UAAU2B,gBAAkB,WACrC7C,EAAEU,sBAAsB2C,IAAI,UAGhC1C,aAAaO,UAAUU,SAAW,SAAS0B,aAAcC,aAAcC,yBAE9DC,YAEUlD,SAASmD,aAAa,mBAAoBJ,aAAchC,KAAKV,gBAAiB2C,cACpFI,KAAK,SAASC,KAAMC,IAEzB7D,EAAEU,0BAA0BkD,KAAKA,MACjCxD,UAAU0D,cAAcD,SAEnB1C,WAAWQ,8BACXP,YAAYO,yBAGjB3B,EAAEU,0BAA0BsB,GAAG,SAAU,OAAQwB,qBAEnD1B,KAAKR,OAAOyC,KAAK5D,aAAa6D,YAGpCrD,aAAaO,UAAUuC,UAAY,WAE/BzD,EAAEU,0BAA0B2C,IAAI,SAAU,SAU9C1C,aAAaO,UAAU+C,WAAa,SAASC,GACzCA,EAAEhC,iBACFlC,EAAEU,0BAA0BwC,KAAK,QAAQiB,UAG7CxD,aAAaO,UAAUa,2BAA6B,SAASmC,QACpDE,eAAeF,EAAG,2CAG3BvD,aAAaO,UAAUW,sBAAwB,SAASqC,QAC/CE,eAAeF,EAAG,sCAG3BvD,aAAaO,UAAUkD,eAAiB,SAASF,EAAGG,gBAEhDH,EAAEhC,qBAGEoC,SAAWtE,EAAEU,0BAA0BwC,KAAK,QAAQqB,mBACjDjD,KAAKC,QAAQiD,MAAK,SAASjD,SAC9BrB,KAAKuE,KAAK,CAAC,CACPC,WAAYL,eACZM,KAAM,CAACC,aAAcC,KAAKC,UAAUR,WACpCX,KAAM,WACFxD,aAAa4E,MAAMxD,QAAQ,GAAIA,QAAQ,KAE3CwC,KAAM5D,aAAa6D,gBAGxBgB,MAAM7E,aAAa6D,YAI1BrD,aAAaO,UAAUiC,UAAY,SAAS8B,WAAYnC,gBAAiBC,eAErE7C,KAAKuE,KAAK,CAAC,CACPC,WAAY,uCACZC,KAAM,CACFO,UAAWpC,gBACXqC,QAASpC,eAEbY,KAAM,SAASnB,MACiB,GAAxBA,KAAK4C,SAASC,OAIlBjF,UAAUkF,OAAO,yCAA0C9C,MACtDgC,KAAK,SAASZ,MACXqB,WAAWM,MAAM3B,WACZf,uBACAlB,8BACAyB,OAAO6B,YACZA,WAAWzC,KAAK,SAAU,IAE5BV,KAAKR,OACNyC,KAAK5D,aAAa6D,gBAZdwB,WAAWP,WAAYlC,gBAalCjB,KAAKR,MACPyC,KAAM5D,aAAa6D,cAI3BrD,aAAaO,UAAUsE,WAAa,SAASC,KAAM1C,eAC/C0C,KAAKjD,KAAK,kBAAmB,IAC7BiD,KAAKjD,KAAK,gBAAiB,SACtBjB,QAAQiD,MAAK,SAASjD,aAGnBE,IAAM,EACW,UAAjBsB,cACAtB,IAAM,EACkB,UAAjBsB,gBACPtB,IAAM,GAEVgE,KAAKC,KAAKnE,QAAQE,SAEnBsC,KAAK5D,aAAa6D,YAGzBrD,aAAaO,UAAU+B,SAAW,SAASwC,MACvCA,KAAKjD,KAAK,WAAY,GACtBiD,KAAKE,SAAS,OAAOrD,SAAS,UAC9BmD,KAAKvC,KAAK,OAAOb,YAAY,YAC7BoD,KAAKvC,KAAK,OAAOZ,SAAS,YAG9B3B,aAAaO,UAAUkC,OAAS,SAASqC,MACrCA,KAAKjD,KAAK,WAAY,GACtBiD,KAAKE,SAAS,OAAOtD,YAAY,UACjCoD,KAAKvC,KAAK,OAAOb,YAAY,WAE7BoD,KAAKvC,KAAK,OAAOb,YAAY,6BAC7BoD,KAAKvC,KAAK,OAAOZ,SAAS,aAE6B,CAUvDrB,KAAM,SAASL,gBAAiBC,iBAAkBC,sBACvC,IAAIH,aAAaC,gBAAiBC,iBAAkBC"} amd/build/form-user-selector.min.js 0000644 00000001757 15152701722 0013275 0 ustar 00 /** * Potential user selector module. * * @module tool_dataprivacy/form-user-selector * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define("tool_dataprivacy/form-user-selector",["jquery","core/ajax","core/templates"],(function($,Ajax,Templates){return{processResults:function(selector,results){var users=[];return $.each(results,(function(index,user){users.push({value:user.id,label:user._label})})),users},transport:function(selector,query,success,failure){Ajax.call([{methodname:"tool_dataprivacy_get_users",args:{query:query}}])[0].then((function(results){var promises=[],i=0;return $.each(results,(function(index,user){promises.push(Templates.render("tool_dataprivacy/form-user-selector-suggestion",user))})),$.when.apply($.when,promises).then((function(){var args=arguments;$.each(results,(function(index,user){user._label=args[i],i++})),success(results)}))})).fail(failure)}}})); //# sourceMappingURL=form-user-selector.min.js.map amd/build/defaultsactions.min.js 0000644 00000013311 15152701722 0012715 0 ustar 00 /** * AMD module for data registry defaults actions. * * @module tool_dataprivacy/defaultsactions * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define("tool_dataprivacy/defaultsactions",["jquery","core/ajax","core/notification","core/str","core/modal_factory","core/modal_events","core/templates"],(function($,Ajax,Notification,Str,ModalFactory,ModalEvents,Templates){var ACTIONS_EDIT_LEVEL_DEFAULTS='[data-action="edit-level-defaults"]',ACTIONS_NEW_ACTIVITY_DEFAULTS='[data-action="new-activity-defaults"]',ACTIONS_EDIT_ACTIVITY_DEFAULTS='[data-action="edit-activity-defaults"]',ACTIONS_DELETE_ACTIVITY_DEFAULTS='[data-action="delete-activity-defaults"]',DefaultsActions=function(){this.registerEvents()};function showDefaultsFormModal(title,contextLevel,category,purpose,activity,categoryOptions,purposeOptions,activityOptions){null!==category&&categoryOptions.forEach((function(currentValue){currentValue.id===category&&(currentValue.selected=!0)})),null!==purpose&&purposeOptions.forEach((function(currentValue){currentValue.id===purpose&&(currentValue.selected=!0)}));var templateContext={contextlevel:contextLevel,categoryoptions:categoryOptions,purposeoptions:purposeOptions};null!==activityOptions&&activityOptions.length&&(null===activity?templateContext.newactivitydefaults=!0:activityOptions.forEach((function(currentValue){activity===currentValue.name&&(currentValue.selected=!0)})),templateContext.modemodule=!0,templateContext.activityoptions=activityOptions),ModalFactory.create({title:title,body:Templates.render("tool_dataprivacy/category_purpose_form",templateContext),type:ModalFactory.types.SAVE_CANCEL,large:!0}).then((function(modal){return modal.getRoot().on(ModalEvents.save,(function(){var activity=$("#activity"),activityVal=void 0!==activity?activity.val():null,override=$("#override"),overrideVal=void 0!==override&&override.is(":checked");setContextDefaults($("#contextlevel").val(),$("#category").val(),$("#purpose").val(),activityVal,overrideVal)})),modal.getRoot().on(ModalEvents.hidden,(function(){modal.destroy()})),modal.show(),modal})).catch(Notification.exception)}function setContextDefaults(contextLevel,category,purpose,activity,override){var request={methodname:"tool_dataprivacy_set_context_defaults",args:{contextlevel:contextLevel,category:category,purpose:purpose,override:override,activity:activity}};Ajax.call([request])[0].done((function(data){data.result&&window.location.reload()}))}return DefaultsActions.prototype.registerEvents=function(){$(ACTIONS_EDIT_LEVEL_DEFAULTS).click((function(e){e.preventDefault();var button=$(this),contextLevel=button.data("contextlevel"),category=button.data("category"),purpose=button.data("purpose"),promises=Ajax.call([{methodname:"tool_dataprivacy_get_category_options",args:{}},{methodname:"tool_dataprivacy_get_purpose_options",args:{}}]),titlePromise=Str.get_string("editdefaults","tool_dataprivacy",$("#defaults-header").text());$.when(promises[0],promises[1],titlePromise).then((function(categoryResponse,purposeResponse,title){var categories=categoryResponse.options,purposes=purposeResponse.options;return showDefaultsFormModal(title,contextLevel,category,purpose,null,categories,purposes,null),!0})).catch(Notification.exception)})),$(ACTIONS_NEW_ACTIVITY_DEFAULTS).click((function(e){e.preventDefault();var contextLevel=$(this).data("contextlevel"),promises=Ajax.call([{methodname:"tool_dataprivacy_get_category_options",args:{}},{methodname:"tool_dataprivacy_get_purpose_options",args:{}},{methodname:"tool_dataprivacy_get_activity_options",args:{nodefaults:!0}}]),titlePromise=Str.get_string("addnewdefaults","tool_dataprivacy");$.when(promises[0],promises[1],promises[2],titlePromise).then((function(categoryResponse,purposeResponse,activityResponse,title){var categories=categoryResponse.options,purposes=purposeResponse.options,activities=activityResponse.options;return showDefaultsFormModal(title,contextLevel,null,null,null,categories,purposes,activities),!0})).catch(Notification.exception)})),$(ACTIONS_EDIT_ACTIVITY_DEFAULTS).click((function(e){e.preventDefault();var button=$(this),contextLevel=button.data("contextlevel"),category=button.data("category"),purpose=button.data("purpose"),activity=button.data("activityname"),promises=Ajax.call([{methodname:"tool_dataprivacy_get_category_options",args:{}},{methodname:"tool_dataprivacy_get_purpose_options",args:{}},{methodname:"tool_dataprivacy_get_activity_options",args:{}}]),titlePromise=Str.get_string("editmoduledefaults","tool_dataprivacy");$.when(promises[0],promises[1],promises[2],titlePromise).then((function(categoryResponse,purposeResponse,activityResponse,title){var categories=categoryResponse.options,purposes=purposeResponse.options,activities=activityResponse.options;return showDefaultsFormModal(title,contextLevel,category,purpose,activity,categories,purposes,activities),!0})).catch(Notification.exception)})),$(ACTIONS_DELETE_ACTIVITY_DEFAULTS).click((function(e){e.preventDefault();var button=$(this),contextLevel=button.data("contextlevel"),activity=button.data("activityname"),activityDisplayName=button.data("activitydisplayname");ModalFactory.create({title:Str.get_string("deletedefaults","tool_dataprivacy",activityDisplayName),body:Templates.render("tool_dataprivacy/delete_activity_defaults",{activityname:activityDisplayName}),type:ModalFactory.types.SAVE_CANCEL,large:!0}).then((function(modal){return modal.setSaveButtonText(Str.get_string("delete")),modal.getRoot().on(ModalEvents.save,(function(){setContextDefaults(contextLevel,-1,-1,activity,!1)})),modal.getRoot().on(ModalEvents.hidden,(function(){modal.destroy()})),modal.show(),!0})).catch(Notification.exception)}))},{init:function(){return new DefaultsActions}}})); //# sourceMappingURL=defaultsactions.min.js.map amd/build/add_category.min.js 0000644 00000005131 15152701722 0012153 0 ustar 00 /** * Module to add categories. * * @module tool_dataprivacy/add_category * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define("tool_dataprivacy/add_category",["jquery","core/str","core/ajax","core/notification","core/modal_factory","core/modal_events","core/fragment","core_form/changechecker"],(function($,Str,Ajax,Notification,ModalFactory,ModalEvents,Fragment,FormChangeChecker){var SELECTORS_CATEGORY_LINK='[data-add-element="category"]',AddCategory=function(contextId){this.contextId=contextId;this.strings=Str.get_strings([{key:"addcategory",component:"tool_dataprivacy"},{key:"save",component:"admin"}]),this.registerEventListeners()};return AddCategory.prototype.contextId=0,AddCategory.prototype.strings=0,AddCategory.prototype.registerEventListeners=function(){var trigger=$(SELECTORS_CATEGORY_LINK);trigger.on("click",function(){return this.strings.then(function(strings){ModalFactory.create({type:ModalFactory.types.SAVE_CANCEL,title:strings[0],body:""},trigger).done(function(modal){this.setupFormModal(modal,strings[1])}.bind(this))}.bind(this)).fail(Notification.exception)}.bind(this))},AddCategory.prototype.getBody=function(formdata){var params=null;return void 0!==formdata&&(params={jsonformdata:JSON.stringify(formdata)}),Fragment.loadFragment("tool_dataprivacy","addcategory_form",this.contextId,params)},AddCategory.prototype.setupFormModal=function(modal,saveText){modal.setLarge(),modal.setSaveButtonText(saveText),modal.getRoot().on(ModalEvents.hidden,this.destroy.bind(this)),modal.setBody(this.getBody()),modal.getRoot().on(ModalEvents.save,this.submitForm.bind(this)),modal.getRoot().on("submit","form",this.submitFormAjax.bind(this)),this.modal=modal,modal.show()},AddCategory.prototype.submitForm=function(e){e.preventDefault(),this.modal.getRoot().find("form").submit()},AddCategory.prototype.submitFormAjax=function(e){e.preventDefault();var formData=this.modal.getRoot().find("form").serialize();Ajax.call([{methodname:"tool_dataprivacy_create_category_form",args:{jsonformdata:JSON.stringify(formData)},done:function(data){data.validationerrors?this.modal.setBody(this.getBody(formData)):this.close()}.bind(this),fail:Notification.exception}])},AddCategory.prototype.close=function(){this.destroy(),document.location.reload()},AddCategory.prototype.destroy=function(){FormChangeChecker.resetAllFormDirtyStates(),this.modal.destroy()},AddCategory.prototype.removeListeners=function(){$(SELECTORS_CATEGORY_LINK).off("click")},{getInstance:function(contextId){return new AddCategory(contextId)}}})); //# sourceMappingURL=add_category.min.js.map amd/build/request_filter.min.js.map 0000644 00000007156 15152701722 0013350 0 ustar 00 {"version":3,"file":"request_filter.min.js","sources":["../src/request_filter.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * JS module for the data requests filter.\n *\n * @module tool_dataprivacy/request_filter\n * @copyright 2018 Jun Pataleta\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery', 'core/form-autocomplete', 'core/str', 'core/notification'], function($, Autocomplete, Str, Notification) {\n\n /**\n * Selectors.\n *\n * @access private\n * @type {{REQUEST_FILTERS: string}}\n */\n var SELECTORS = {\n REQUEST_FILTERS: '#request-filters'\n };\n\n /**\n * Init function.\n *\n * @method init\n * @private\n */\n var init = function() {\n var stringkeys = [\n {\n key: 'filter',\n component: 'moodle'\n },\n {\n key: 'nofiltersapplied',\n component: 'moodle'\n }\n ];\n\n Str.get_strings(stringkeys).then(function(langstrings) {\n var placeholder = langstrings[0];\n var noSelectionString = langstrings[1];\n return Autocomplete.enhance(SELECTORS.REQUEST_FILTERS, false, '', placeholder, false, true, noSelectionString, true);\n }).fail(Notification.exception);\n\n var last = $(SELECTORS.REQUEST_FILTERS).val();\n $(SELECTORS.REQUEST_FILTERS).on('change', function() {\n var current = $(this).val();\n // Prevent form from submitting unnecessarily, eg. on blur when no filter is selected.\n if (last.join(',') !== current.join(',')) {\n // If we're submitting without filters, set the hidden input 'filters-cleared' to 1.\n if (current.length === 0) {\n $('#filters-cleared').val(1);\n }\n $(this.form).submit();\n }\n });\n };\n\n return /** @alias module:core/form-autocomplete */ {\n /**\n * Initialise the unified user filter.\n *\n * @method init\n */\n init: function() {\n init();\n }\n };\n});\n"],"names":["define","$","Autocomplete","Str","Notification","SELECTORS","init","get_strings","key","component","then","langstrings","placeholder","noSelectionString","enhance","fail","exception","last","val","on","current","this","join","length","form","submit"],"mappings":";;;;;;;AAsBAA,yCAAO,CAAC,SAAU,yBAA0B,WAAY,sBAAsB,SAASC,EAAGC,aAAcC,IAAKC,kBAQrGC,0BACiB,yBAyC8B,CAM/CC,KAAM,YAtCC,WAYPH,IAAII,YAXa,CACb,CACIC,IAAK,SACLC,UAAW,UAEf,CACID,IAAK,mBACLC,UAAW,YAISC,MAAK,SAASC,iBAClCC,YAAcD,YAAY,GAC1BE,kBAAoBF,YAAY,UAC7BT,aAAaY,QAAQT,2BAA2B,EAAO,GAAIO,aAAa,GAAO,EAAMC,mBAAmB,MAChHE,KAAKX,aAAaY,eAEjBC,KAAOhB,EAAEI,2BAA2Ba,MACxCjB,EAAEI,2BAA2Bc,GAAG,UAAU,eAClCC,QAAUnB,EAAEoB,MAAMH,MAElBD,KAAKK,KAAK,OAASF,QAAQE,KAAK,OAET,IAAnBF,QAAQG,QACRtB,EAAE,oBAAoBiB,IAAI,GAE9BjB,EAAEoB,KAAKG,MAAMC,aAYjBnB"} amd/build/data_request_modal.min.js.map 0000644 00000012435 15152701722 0014144 0 ustar 00 {"version":3,"file":"data_request_modal.min.js","sources":["../src/data_request_modal.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Request actions.\n *\n * @module tool_dataprivacy/data_request_modal\n * @copyright 2018 Jun Pataleta\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery', 'core/notification', 'core/custom_interaction_events', 'core/modal', 'core/modal_registry',\n 'tool_dataprivacy/events'],\n function($, Notification, CustomEvents, Modal, ModalRegistry, DataPrivacyEvents) {\n\n var registered = false;\n var SELECTORS = {\n APPROVE_BUTTON: '[data-action=\"approve\"]',\n DENY_BUTTON: '[data-action=\"deny\"]',\n COMPLETE_BUTTON: '[data-action=\"complete\"]'\n };\n\n /**\n * Constructor for the Modal.\n *\n * @param {object} root The root jQuery element for the modal\n */\n var ModalDataRequest = function(root) {\n Modal.call(this, root);\n };\n\n ModalDataRequest.TYPE = 'tool_dataprivacy-data_request';\n ModalDataRequest.prototype = Object.create(Modal.prototype);\n ModalDataRequest.prototype.constructor = ModalDataRequest;\n\n /**\n * Set up all of the event handling for the modal.\n *\n * @method registerEventListeners\n */\n ModalDataRequest.prototype.registerEventListeners = function() {\n // Apply parent event listeners.\n Modal.prototype.registerEventListeners.call(this);\n\n this.getModal().on(CustomEvents.events.activate, SELECTORS.APPROVE_BUTTON, function(e, data) {\n var approveEvent = $.Event(DataPrivacyEvents.approve);\n this.getRoot().trigger(approveEvent, this);\n\n if (!approveEvent.isDefaultPrevented()) {\n this.hide();\n data.originalEvent.preventDefault();\n }\n }.bind(this));\n\n this.getModal().on(CustomEvents.events.activate, SELECTORS.DENY_BUTTON, function(e, data) {\n var denyEvent = $.Event(DataPrivacyEvents.deny);\n this.getRoot().trigger(denyEvent, this);\n\n if (!denyEvent.isDefaultPrevented()) {\n this.hide();\n data.originalEvent.preventDefault();\n }\n }.bind(this));\n\n this.getModal().on(CustomEvents.events.activate, SELECTORS.COMPLETE_BUTTON, function(e, data) {\n var completeEvent = $.Event(DataPrivacyEvents.complete);\n this.getRoot().trigger(completeEvent, this);\n\n if (!completeEvent.isDefaultPrevented()) {\n this.hide();\n data.originalEvent.preventDefault();\n }\n }.bind(this));\n };\n\n // Automatically register with the modal registry the first time this module is imported so that you can create modals\n // of this type using the modal factory.\n if (!registered) {\n ModalRegistry.register(ModalDataRequest.TYPE, ModalDataRequest, 'tool_dataprivacy/data_request_modal');\n registered = true;\n }\n\n return ModalDataRequest;\n });"],"names":["define","$","Notification","CustomEvents","Modal","ModalRegistry","DataPrivacyEvents","registered","SELECTORS","ModalDataRequest","root","call","this","TYPE","prototype","Object","create","constructor","registerEventListeners","getModal","on","events","activate","e","data","approveEvent","Event","approve","getRoot","trigger","isDefaultPrevented","hide","originalEvent","preventDefault","bind","denyEvent","deny","completeEvent","complete","register"],"mappings":";;;;;;;AAsBAA,6CAAO,CAAC,SAAU,oBAAqB,iCAAkC,aAAc,sBAC/E,4BACJ,SAASC,EAAGC,aAAcC,aAAcC,MAAOC,cAAeC,uBAEtDC,YAAa,EACbC,yBACgB,0BADhBA,sBAEa,uBAFbA,0BAGiB,2BAQjBC,iBAAmB,SAASC,MAC5BN,MAAMO,KAAKC,KAAMF,cAGrBD,iBAAiBI,KAAO,iCACxBJ,iBAAiBK,UAAYC,OAAOC,OAAOZ,MAAMU,YACtBG,YAAcR,iBAOzCA,iBAAiBK,UAAUI,uBAAyB,WAEhDd,MAAMU,UAAUI,uBAAuBP,KAAKC,WAEvCO,WAAWC,GAAGjB,aAAakB,OAAOC,SAAUd,yBAA0B,SAASe,EAAGC,UAC/EC,aAAexB,EAAEyB,MAAMpB,kBAAkBqB,cACxCC,UAAUC,QAAQJ,aAAcb,MAEhCa,aAAaK,4BACTC,OACLP,KAAKQ,cAAcC,mBAEzBC,KAAKtB,YAEFO,WAAWC,GAAGjB,aAAakB,OAAOC,SAAUd,sBAAuB,SAASe,EAAGC,UAC5EW,UAAYlC,EAAEyB,MAAMpB,kBAAkB8B,WACrCR,UAAUC,QAAQM,UAAWvB,MAE7BuB,UAAUL,4BACNC,OACLP,KAAKQ,cAAcC,mBAEzBC,KAAKtB,YAEFO,WAAWC,GAAGjB,aAAakB,OAAOC,SAAUd,0BAA2B,SAASe,EAAGC,UAChFa,cAAgBpC,EAAEyB,MAAMpB,kBAAkBgC,eACzCV,UAAUC,QAAQQ,cAAezB,MAEjCyB,cAAcP,4BACVC,OACLP,KAAKQ,cAAcC,mBAEzBC,KAAKtB,QAKNL,aACDF,cAAckC,SAAS9B,iBAAiBI,KAAMJ,iBAAkB,uCAChEF,YAAa,GAGVE"} amd/src/request_filter.js 0000644 00000005226 15152701722 0011476 0 ustar 00 // 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/>. /** * JS module for the data requests filter. * * @module tool_dataprivacy/request_filter * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define(['jquery', 'core/form-autocomplete', 'core/str', 'core/notification'], function($, Autocomplete, Str, Notification) { /** * Selectors. * * @access private * @type {{REQUEST_FILTERS: string}} */ var SELECTORS = { REQUEST_FILTERS: '#request-filters' }; /** * Init function. * * @method init * @private */ var init = function() { var stringkeys = [ { key: 'filter', component: 'moodle' }, { key: 'nofiltersapplied', component: 'moodle' } ]; Str.get_strings(stringkeys).then(function(langstrings) { var placeholder = langstrings[0]; var noSelectionString = langstrings[1]; return Autocomplete.enhance(SELECTORS.REQUEST_FILTERS, false, '', placeholder, false, true, noSelectionString, true); }).fail(Notification.exception); var last = $(SELECTORS.REQUEST_FILTERS).val(); $(SELECTORS.REQUEST_FILTERS).on('change', function() { var current = $(this).val(); // Prevent form from submitting unnecessarily, eg. on blur when no filter is selected. if (last.join(',') !== current.join(',')) { // If we're submitting without filters, set the hidden input 'filters-cleared' to 1. if (current.length === 0) { $('#filters-cleared').val(1); } $(this.form).submit(); } }); }; return /** @alias module:core/form-autocomplete */ { /** * Initialise the unified user filter. * * @method init */ init: function() { init(); } }; }); amd/src/requestactions.js 0000644 00000033261 15152701722 0011512 0 ustar 00 // 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/>. /** * Request actions. * * @module tool_dataprivacy/requestactions * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define([ 'jquery', 'core/ajax', 'core/notification', 'core/str', 'core/modal_factory', 'core/modal_events', 'core/templates', 'tool_dataprivacy/data_request_modal', 'tool_dataprivacy/events'], function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, ModalDataRequest, DataPrivacyEvents) { /** * List of action selectors. * * @type {{APPROVE_REQUEST: string}} * @type {{DENY_REQUEST: string}} * @type {{VIEW_REQUEST: string}} * @type {{MARK_COMPLETE: string}} * @type {{CHANGE_BULK_ACTION: string}} * @type {{CONFIRM_BULK_ACTION: string}} * @type {{SELECT_ALL: string}} */ var ACTIONS = { APPROVE_REQUEST: '[data-action="approve"]', DENY_REQUEST: '[data-action="deny"]', VIEW_REQUEST: '[data-action="view"]', MARK_COMPLETE: '[data-action="complete"]', CHANGE_BULK_ACTION: '[id="bulk-action"]', CONFIRM_BULK_ACTION: '[id="confirm-bulk-action"]', SELECT_ALL: '[data-action="selectall"]' }; /** * List of available bulk actions. * * @type {{APPROVE: number}} * @type {{DENY: number}} */ var BULK_ACTIONS = { APPROVE: 1, DENY: 2 }; /** * List of selectors. * * @type {{SELECT_REQUEST: string}} */ var SELECTORS = { SELECT_REQUEST: '.selectrequests' }; /** * RequestActions class. */ var RequestActions = function() { this.registerEvents(); }; /** * Register event listeners. */ RequestActions.prototype.registerEvents = function() { $(ACTIONS.VIEW_REQUEST).click(function(e) { e.preventDefault(); var requestId = $(this).data('requestid'); // Cancel the request. var params = { 'requestid': requestId }; var request = { methodname: 'tool_dataprivacy_get_data_request', args: params }; var promises = Ajax.call([request]); $.when(promises[0]).then(function(data) { if (data.result) { return data.result; } // Fail. Notification.addNotification({ message: data.warnings[0].message, type: 'error' }); return false; }).then(function(data) { var body = Templates.render('tool_dataprivacy/request_details', data); var templateContext = { approvedeny: data.approvedeny, canmarkcomplete: data.canmarkcomplete }; return ModalFactory.create({ title: data.typename, body: body, type: ModalDataRequest.TYPE, large: true, templateContext: templateContext }); }).then(function(modal) { // Handle approve event. modal.getRoot().on(DataPrivacyEvents.approve, function() { showConfirmation(DataPrivacyEvents.approve, approveEventWsData(requestId)); }); // Handle deny event. modal.getRoot().on(DataPrivacyEvents.deny, function() { showConfirmation(DataPrivacyEvents.deny, denyEventWsData(requestId)); }); // Handle send event. modal.getRoot().on(DataPrivacyEvents.complete, function() { var params = { 'requestid': requestId }; handleSave('tool_dataprivacy_mark_complete', params); }); // Handle hidden event. modal.getRoot().on(ModalEvents.hidden, function() { // Destroy when hidden. modal.destroy(); }); // Show the modal! modal.show(); return; }).catch(Notification.exception); }); $(ACTIONS.APPROVE_REQUEST).click(function(e) { e.preventDefault(); var requestId = $(this).data('requestid'); showConfirmation(DataPrivacyEvents.approve, approveEventWsData(requestId)); }); $(ACTIONS.DENY_REQUEST).click(function(e) { e.preventDefault(); var requestId = $(this).data('requestid'); showConfirmation(DataPrivacyEvents.deny, denyEventWsData(requestId)); }); $(ACTIONS.MARK_COMPLETE).click(function(e) { e.preventDefault(); var requestId = $(this).data('requestid'); showConfirmation(DataPrivacyEvents.complete, completeEventWsData(requestId)); }); $(ACTIONS.CONFIRM_BULK_ACTION).click(function() { var requestIds = []; var actionEvent = ''; var wsdata = {}; var bulkActionKeys = [ { key: 'selectbulkaction', component: 'tool_dataprivacy' }, { key: 'selectdatarequests', component: 'tool_dataprivacy' }, { key: 'ok' } ]; var bulkaction = parseInt($('#bulk-action').val()); if (bulkaction != BULK_ACTIONS.APPROVE && bulkaction != BULK_ACTIONS.DENY) { Str.get_strings(bulkActionKeys).done(function(langStrings) { Notification.alert('', langStrings[0], langStrings[2]); }).fail(Notification.exception); return; } $(".selectrequests:checked").each(function() { requestIds.push($(this).val()); }); if (requestIds.length < 1) { Str.get_strings(bulkActionKeys).done(function(langStrings) { Notification.alert('', langStrings[1], langStrings[2]); }).fail(Notification.exception); return; } switch (bulkaction) { case BULK_ACTIONS.APPROVE: actionEvent = DataPrivacyEvents.bulkApprove; wsdata = bulkApproveEventWsData(requestIds); break; case BULK_ACTIONS.DENY: actionEvent = DataPrivacyEvents.bulkDeny; wsdata = bulkDenyEventWsData(requestIds); } showConfirmation(actionEvent, wsdata); }); $(ACTIONS.SELECT_ALL).change(function(e) { e.preventDefault(); var selectAll = $(this).is(':checked'); $(SELECTORS.SELECT_REQUEST).prop('checked', selectAll); }); }; /** * Return the webservice data for the approve request action. * * @param {Number} requestId The ID of the request. * @return {Object} */ function approveEventWsData(requestId) { return { 'wsfunction': 'tool_dataprivacy_approve_data_request', 'wsparams': {'requestid': requestId} }; } /** * Return the webservice data for the bulk approve request action. * * @param {Array} requestIds The array of request ID's. * @return {Object} */ function bulkApproveEventWsData(requestIds) { return { 'wsfunction': 'tool_dataprivacy_bulk_approve_data_requests', 'wsparams': {'requestids': requestIds} }; } /** * Return the webservice data for the deny request action. * * @param {Number} requestId The ID of the request. * @return {Object} */ function denyEventWsData(requestId) { return { 'wsfunction': 'tool_dataprivacy_deny_data_request', 'wsparams': {'requestid': requestId} }; } /** * Return the webservice data for the bulk deny request action. * * @param {Array} requestIds The array of request ID's. * @return {Object} */ function bulkDenyEventWsData(requestIds) { return { 'wsfunction': 'tool_dataprivacy_bulk_deny_data_requests', 'wsparams': {'requestids': requestIds} }; } /** * Return the webservice data for the complete request action. * * @param {Number} requestId The ID of the request. * @return {Object} */ function completeEventWsData(requestId) { return { 'wsfunction': 'tool_dataprivacy_mark_complete', 'wsparams': {'requestid': requestId} }; } /** * Show the confirmation dialogue. * * @param {String} action The action name. * @param {Object} wsdata Object containing ws data. */ function showConfirmation(action, wsdata) { var keys = []; switch (action) { case DataPrivacyEvents.approve: keys = [ { key: 'approverequest', component: 'tool_dataprivacy' }, { key: 'confirmapproval', component: 'tool_dataprivacy' } ]; break; case DataPrivacyEvents.bulkApprove: keys = [ { key: 'bulkapproverequests', component: 'tool_dataprivacy' }, { key: 'confirmbulkapproval', component: 'tool_dataprivacy' } ]; break; case DataPrivacyEvents.deny: keys = [ { key: 'denyrequest', component: 'tool_dataprivacy' }, { key: 'confirmdenial', component: 'tool_dataprivacy' } ]; break; case DataPrivacyEvents.bulkDeny: keys = [ { key: 'bulkdenyrequests', component: 'tool_dataprivacy' }, { key: 'confirmbulkdenial', component: 'tool_dataprivacy' } ]; break; case DataPrivacyEvents.complete: keys = [ { key: 'markcomplete', component: 'tool_dataprivacy' }, { key: 'confirmcompletion', component: 'tool_dataprivacy' } ]; break; } var modalTitle = ''; Str.get_strings(keys).then(function(langStrings) { modalTitle = langStrings[0]; var confirmMessage = langStrings[1]; return ModalFactory.create({ title: modalTitle, body: confirmMessage, type: ModalFactory.types.SAVE_CANCEL }); }).then(function(modal) { modal.setSaveButtonText(modalTitle); // Handle save event. modal.getRoot().on(ModalEvents.save, function() { handleSave(wsdata.wsfunction, wsdata.wsparams); }); // Handle hidden event. modal.getRoot().on(ModalEvents.hidden, function() { // Destroy when hidden. modal.destroy(); }); modal.show(); return; }).catch(Notification.exception); } /** * Calls a web service function and reloads the page on success and shows a notification. * Displays an error notification, otherwise. * * @param {String} wsfunction The web service function to call. * @param {Object} params The parameters for the web service functoon. */ function handleSave(wsfunction, params) { // Confirm the request. var request = { methodname: wsfunction, args: params }; Ajax.call([request])[0].done(function(data) { if (data.result) { // On success, reload the page so that the data request table will be updated. // TODO: Probably in the future, better to reload the table or the target data request via AJAX. window.location.reload(); } else { // Add the notification. Notification.addNotification({ message: data.warnings[0].message, type: 'error' }); } }).fail(Notification.exception); } return RequestActions; }); amd/src/data_deletion.js 0000644 00000011015 15152701722 0011226 0 ustar 00 // 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/>. /** * Request actions. * * @module tool_dataprivacy/data_deletion * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define([ 'jquery', 'core/ajax', 'core/notification', 'core/str', 'core/modal_factory', 'core/modal_events'], function($, Ajax, Notification, Str, ModalFactory, ModalEvents) { /** * List of action selectors. * * @type {{MARK_FOR_DELETION: string}} * @type {{SELECT_ALL: string}} */ var ACTIONS = { MARK_FOR_DELETION: '[data-action="markfordeletion"]', SELECT_ALL: '[data-action="selectall"]', }; /** * List of selectors. * * @type {{SELECTCONTEXT: string}} */ var SELECTORS = { SELECTCONTEXT: '.selectcontext', }; /** * DataDeletionActions class. */ var DataDeletionActions = function() { this.registerEvents(); }; /** * Register event listeners. */ DataDeletionActions.prototype.registerEvents = function() { $(ACTIONS.MARK_FOR_DELETION).click(function(e) { e.preventDefault(); var selectedIds = []; $(SELECTORS.SELECTCONTEXT).each(function() { var checkbox = $(this); if (checkbox.is(':checked')) { selectedIds.push(checkbox.val()); } }); showConfirmation(selectedIds); }); $(ACTIONS.SELECT_ALL).change(function(e) { e.preventDefault(); var selectallnone = $(this); if (selectallnone.is(':checked')) { $(SELECTORS.SELECTCONTEXT).attr('checked', 'checked'); } else { $(SELECTORS.SELECTCONTEXT).removeAttr('checked'); } }); }; /** * Show the confirmation dialogue. * * @param {Array} ids The array of expired context record IDs. */ function showConfirmation(ids) { var keys = [ { key: 'confirm', component: 'moodle' }, { key: 'confirmcontextdeletion', component: 'tool_dataprivacy' } ]; var wsfunction = 'tool_dataprivacy_confirm_contexts_for_deletion'; var modalTitle = ''; Str.get_strings(keys).then(function(langStrings) { modalTitle = langStrings[0]; var confirmMessage = langStrings[1]; return ModalFactory.create({ title: modalTitle, body: confirmMessage, type: ModalFactory.types.SAVE_CANCEL }); }).then(function(modal) { modal.setSaveButtonText(modalTitle); // Handle save event. modal.getRoot().on(ModalEvents.save, function() { // Confirm the request. var params = { 'ids': ids }; var request = { methodname: wsfunction, args: params }; Ajax.call([request])[0].done(function(data) { if (data.result) { window.location.reload(); } else { Notification.addNotification({ message: data.warnings[0].message, type: 'error' }); } }).fail(Notification.exception); }); // Handle hidden event. modal.getRoot().on(ModalEvents.hidden, function() { // Destroy when hidden. modal.destroy(); }); return modal; }).done(function(modal) { modal.show(); }).fail(Notification.exception); } return DataDeletionActions; }); amd/src/data_registry.js 0000644 00000027702 15152701722 0011305 0 ustar 00 // 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/>. /** * Request actions. * * @module tool_dataprivacy/data_registry * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define(['jquery', 'core/str', 'core/ajax', 'core/notification', 'core/templates', 'core/modal_factory', 'core/modal_events', 'core/fragment', 'tool_dataprivacy/add_purpose', 'tool_dataprivacy/add_category'], function($, Str, Ajax, Notification, Templates, ModalFactory, ModalEvents, Fragment, AddPurpose, AddCategory) { var SELECTORS = { TREE_NODES: '[data-context-tree-node=1]', FORM_CONTAINER: '#context-form-container', }; var DataRegistry = function(systemContextId, initContextLevel, initContextId) { this.systemContextId = systemContextId; this.currentContextLevel = initContextLevel; this.currentContextId = initContextId; this.init(); }; /** * @var {int} systemContextId * @private */ DataRegistry.prototype.systemContextId = 0; /** * @var {int} currentContextLevel * @private */ DataRegistry.prototype.currentContextLevel = 0; /** * @var {int} currentContextId * @private */ DataRegistry.prototype.currentContextId = 0; /** * @var {AddPurpose} addpurpose * @private */ DataRegistry.prototype.addpurpose = null; /** * @var {AddCategory} addcategory * @private */ DataRegistry.prototype.addcategory = null; DataRegistry.prototype.init = function() { // Add purpose and category modals always at system context. this.addpurpose = AddPurpose.getInstance(this.systemContextId); this.addcategory = AddCategory.getInstance(this.systemContextId); var stringKeys = [ { key: 'changessaved', component: 'moodle' }, { key: 'contextpurposecategorysaved', component: 'tool_dataprivacy' }, { key: 'noblockstoload', component: 'tool_dataprivacy' }, { key: 'noactivitiestoload', component: 'tool_dataprivacy' }, { key: 'nocoursestoload', component: 'tool_dataprivacy' } ]; this.strings = Str.get_strings(stringKeys); this.registerEventListeners(); // Load the default context level form. if (this.currentContextId) { this.loadForm('context_form', [this.currentContextId], this.submitContextFormAjax.bind(this)); } else { this.loadForm('contextlevel_form', [this.currentContextLevel], this.submitContextLevelFormAjax.bind(this)); } }; DataRegistry.prototype.registerEventListeners = function() { $(SELECTORS.TREE_NODES).on('click', function(ev) { ev.preventDefault(); var trigger = $(ev.currentTarget); // Active node. $(SELECTORS.TREE_NODES).removeClass('active'); trigger.addClass('active'); var contextLevel = trigger.data('contextlevel'); var contextId = trigger.data('contextid'); if (contextLevel) { // Context level level. window.history.pushState({}, null, '?contextlevel=' + contextLevel); // Remove previous add purpose and category listeners to avoid memory leaks. this.addpurpose.removeListeners(); this.addcategory.removeListeners(); // Load the context level form. this.currentContextLevel = contextLevel; this.loadForm('contextlevel_form', [this.currentContextLevel], this.submitContextLevelFormAjax.bind(this)); } else if (contextId) { // Context instance level. window.history.pushState({}, null, '?contextid=' + contextId); // Remove previous add purpose and category listeners to avoid memory leaks. this.addpurpose.removeListeners(); this.addcategory.removeListeners(); // Load the context level form. this.currentContextId = contextId; this.loadForm('context_form', [this.currentContextId], this.submitContextFormAjax.bind(this)); } else { // Expandable nodes. var expandContextId = trigger.data('expandcontextid'); var expandElement = trigger.data('expandelement'); var expanded = trigger.data('expanded'); // Extra checking that there is an expandElement because we remove it after loading 0 branches. if (expandElement) { if (!expanded) { if (trigger.data('loaded') || !expandContextId || !expandElement) { this.expand(trigger); } else { trigger.find('> i').removeClass('fa-plus'); trigger.find('> i').addClass('fa-circle-o-notch fa-spin'); this.loadExtra(trigger, expandContextId, expandElement); } } else { this.collapse(trigger); } } } }.bind(this)); }; DataRegistry.prototype.removeListeners = function() { $(SELECTORS.TREE_NODES).off('click'); }; DataRegistry.prototype.loadForm = function(fragmentName, fragmentArgs, formSubmitCallback) { this.clearForm(); var fragment = Fragment.loadFragment('tool_dataprivacy', fragmentName, this.systemContextId, fragmentArgs); fragment.done(function(html, js) { $(SELECTORS.FORM_CONTAINER).html(html); Templates.runTemplateJS(js); this.addpurpose.registerEventListeners(); this.addcategory.registerEventListeners(); // We also catch the form submit event and use it to submit the form with ajax. $(SELECTORS.FORM_CONTAINER).on('submit', 'form', formSubmitCallback); }.bind(this)).fail(Notification.exception); }; DataRegistry.prototype.clearForm = function() { // Remove previous listeners. $(SELECTORS.FORM_CONTAINER).off('submit', 'form'); }; /** * This triggers a form submission, so that any mform elements can do final tricks before the form submission is processed. * * @method submitForm * @param {Event} e Form submission event. * @private */ DataRegistry.prototype.submitForm = function(e) { e.preventDefault(); $(SELECTORS.FORM_CONTAINER).find('form').submit(); }; DataRegistry.prototype.submitContextLevelFormAjax = function(e) { this.submitFormAjax(e, 'tool_dataprivacy_set_contextlevel_form'); }; DataRegistry.prototype.submitContextFormAjax = function(e) { this.submitFormAjax(e, 'tool_dataprivacy_set_context_form'); }; DataRegistry.prototype.submitFormAjax = function(e, saveMethodName) { // We don't want to do a real form submission. e.preventDefault(); // Convert all the form elements values to a serialised string. var formData = $(SELECTORS.FORM_CONTAINER).find('form').serialize(); return this.strings.then(function(strings) { Ajax.call([{ methodname: saveMethodName, args: {jsonformdata: JSON.stringify(formData)}, done: function() { Notification.alert(strings[0], strings[1]); }, fail: Notification.exception }]); return; }).catch(Notification.exception); }; DataRegistry.prototype.loadExtra = function(parentNode, expandContextId, expandElement) { Ajax.call([{ methodname: 'tool_dataprivacy_tree_extra_branches', args: { contextid: expandContextId, element: expandElement, }, done: function(data) { if (data.branches.length == 0) { this.noElements(parentNode, expandElement); return; } Templates.render('tool_dataprivacy/context_tree_branches', data) .then(function(html) { parentNode.after(html); this.removeListeners(); this.registerEventListeners(); this.expand(parentNode); parentNode.data('loaded', 1); return; }.bind(this)) .fail(Notification.exception); }.bind(this), fail: Notification.exception }]); }; DataRegistry.prototype.noElements = function(node, expandElement) { node.data('expandcontextid', ''); node.data('expandelement', ''); this.strings.then(function(strings) { // 2 = blocks, 3 = activities, 4 = courses (although courses is not likely really). var key = 2; if (expandElement == 'module') { key = 3; } else if (expandElement == 'course') { key = 4; } node.text(strings[key]); return; }).fail(Notification.exception); }; DataRegistry.prototype.collapse = function(node) { node.data('expanded', 0); node.siblings('nav').addClass('hidden'); node.find('> i').removeClass('fa-minus'); node.find('> i').addClass('fa-plus'); }; DataRegistry.prototype.expand = function(node) { node.data('expanded', 1); node.siblings('nav').removeClass('hidden'); node.find('> i').removeClass('fa-plus'); // Also remove the spinning one if data was just loaded. node.find('> i').removeClass('fa-circle-o-notch fa-spin'); node.find('> i').addClass('fa-minus'); }; return /** @alias module:tool_dataprivacy/data_registry */ { /** * Initialise the page. * * @param {Number} systemContextId * @param {Number} initContextLevel * @param {Number} initContextId * @return {DataRegistry} */ init: function(systemContextId, initContextLevel, initContextId) { return new DataRegistry(systemContextId, initContextLevel, initContextId); } }; } ); amd/src/data_request_modal.js 0000644 00000007323 15152701722 0012276 0 ustar 00 // 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/>. /** * Request actions. * * @module tool_dataprivacy/data_request_modal * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define(['jquery', 'core/notification', 'core/custom_interaction_events', 'core/modal', 'core/modal_registry', 'tool_dataprivacy/events'], function($, Notification, CustomEvents, Modal, ModalRegistry, DataPrivacyEvents) { var registered = false; var SELECTORS = { APPROVE_BUTTON: '[data-action="approve"]', DENY_BUTTON: '[data-action="deny"]', COMPLETE_BUTTON: '[data-action="complete"]' }; /** * Constructor for the Modal. * * @param {object} root The root jQuery element for the modal */ var ModalDataRequest = function(root) { Modal.call(this, root); }; ModalDataRequest.TYPE = 'tool_dataprivacy-data_request'; ModalDataRequest.prototype = Object.create(Modal.prototype); ModalDataRequest.prototype.constructor = ModalDataRequest; /** * Set up all of the event handling for the modal. * * @method registerEventListeners */ ModalDataRequest.prototype.registerEventListeners = function() { // Apply parent event listeners. Modal.prototype.registerEventListeners.call(this); this.getModal().on(CustomEvents.events.activate, SELECTORS.APPROVE_BUTTON, function(e, data) { var approveEvent = $.Event(DataPrivacyEvents.approve); this.getRoot().trigger(approveEvent, this); if (!approveEvent.isDefaultPrevented()) { this.hide(); data.originalEvent.preventDefault(); } }.bind(this)); this.getModal().on(CustomEvents.events.activate, SELECTORS.DENY_BUTTON, function(e, data) { var denyEvent = $.Event(DataPrivacyEvents.deny); this.getRoot().trigger(denyEvent, this); if (!denyEvent.isDefaultPrevented()) { this.hide(); data.originalEvent.preventDefault(); } }.bind(this)); this.getModal().on(CustomEvents.events.activate, SELECTORS.COMPLETE_BUTTON, function(e, data) { var completeEvent = $.Event(DataPrivacyEvents.complete); this.getRoot().trigger(completeEvent, this); if (!completeEvent.isDefaultPrevented()) { this.hide(); data.originalEvent.preventDefault(); } }.bind(this)); }; // Automatically register with the modal registry the first time this module is imported so that you can create modals // of this type using the modal factory. if (!registered) { ModalRegistry.register(ModalDataRequest.TYPE, ModalDataRequest, 'tool_dataprivacy/data_request_modal'); registered = true; } return ModalDataRequest; }); amd/src/add_purpose.js 0000644 00000013423 15152701722 0010744 0 ustar 00 // 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/>. /** * Module to add purposes. * * @module tool_dataprivacy/add_purpose * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define([ 'jquery', 'core/str', 'core/ajax', 'core/notification', 'core/modal_factory', 'core/modal_events', 'core/fragment', 'core_form/changechecker', ], function( $, Str, Ajax, Notification, ModalFactory, ModalEvents, Fragment, FormChangeChecker ) { var SELECTORS = { PURPOSE_LINK: '[data-add-element="purpose"]', }; var AddPurpose = function(contextId) { this.contextId = contextId; var stringKeys = [ { key: 'addpurpose', component: 'tool_dataprivacy' }, { key: 'save', component: 'admin' } ]; this.strings = Str.get_strings(stringKeys); this.registerEventListeners(); }; /** * @var {int} contextId * @private */ AddPurpose.prototype.contextId = 0; /** * @var {Promise} * @private */ AddPurpose.prototype.strings = 0; AddPurpose.prototype.registerEventListeners = function() { var trigger = $(SELECTORS.PURPOSE_LINK); trigger.on('click', function() { return this.strings.then(function(strings) { ModalFactory.create({ type: ModalFactory.types.SAVE_CANCEL, title: strings[0], body: '', }, trigger).done(function(modal) { this.setupFormModal(modal, strings[1]); }.bind(this)); }.bind(this)) .fail(Notification.exception); }.bind(this)); }; /** * @method getBody * @param {Object} formdata * @private * @return {Promise} */ AddPurpose.prototype.getBody = function(formdata) { var params = null; if (typeof formdata !== "undefined") { params = {jsonformdata: JSON.stringify(formdata)}; } // Get the content of the modal. return Fragment.loadFragment('tool_dataprivacy', 'addpurpose_form', this.contextId, params); }; AddPurpose.prototype.setupFormModal = function(modal, saveText) { modal.setLarge(); modal.setSaveButtonText(saveText); // We want to reset the form every time it is opened. modal.getRoot().on(ModalEvents.hidden, this.destroy.bind(this)); modal.setBody(this.getBody()); // We catch the modal save event, and use it to submit the form inside the modal. // Triggering a form submission will give JS validation scripts a chance to check for errors. modal.getRoot().on(ModalEvents.save, this.submitForm.bind(this)); // We also catch the form submit event and use it to submit the form with ajax. modal.getRoot().on('submit', 'form', this.submitFormAjax.bind(this)); this.modal = modal; modal.show(); }; /** * This triggers a form submission, so that any mform elements can do final tricks before the form submission is processed. * * @method submitForm * @param {Event} e Form submission event. * @private */ AddPurpose.prototype.submitForm = function(e) { e.preventDefault(); this.modal.getRoot().find('form').submit(); }; AddPurpose.prototype.submitFormAjax = function(e) { // We don't want to do a real form submission. e.preventDefault(); // Convert all the form elements values to a serialised string. var formData = this.modal.getRoot().find('form').serialize(); Ajax.call([{ methodname: 'tool_dataprivacy_create_purpose_form', args: {jsonformdata: JSON.stringify(formData)}, done: function(data) { if (data.validationerrors) { this.modal.setBody(this.getBody(formData)); } else { this.close(); } }.bind(this), fail: Notification.exception }]); }; AddPurpose.prototype.close = function() { this.destroy(); document.location.reload(); }; AddPurpose.prototype.destroy = function() { FormChangeChecker.resetAllFormDirtyStates(); this.modal.destroy(); }; AddPurpose.prototype.removeListeners = function() { $(SELECTORS.PURPOSE_LINK).off('click'); }; return /** @alias module:tool_dataprivacy/add_purpose */ { getInstance: function(contextId) { return new AddPurpose(contextId); } }; } ); amd/src/defaultsactions.js 0000644 00000027033 15152701722 0011631 0 ustar 00 // 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/>. /** * AMD module for data registry defaults actions. * * @module tool_dataprivacy/defaultsactions * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define([ 'jquery', 'core/ajax', 'core/notification', 'core/str', 'core/modal_factory', 'core/modal_events', 'core/templates'], function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates) { /** * List of action selectors. * * @type {{EDIT_LEVEL_DEFAULTS: string}} * @type {{NEW_ACTIVITY_DEFAULTS: string}} * @type {{EDIT_ACTIVITY_DEFAULTS: string}} * @type {{DELETE_ACTIVITY_DEFAULTS: string}} */ var ACTIONS = { EDIT_LEVEL_DEFAULTS: '[data-action="edit-level-defaults"]', NEW_ACTIVITY_DEFAULTS: '[data-action="new-activity-defaults"]', EDIT_ACTIVITY_DEFAULTS: '[data-action="edit-activity-defaults"]', DELETE_ACTIVITY_DEFAULTS: '[data-action="delete-activity-defaults"]' }; /** @type {{INHERIT: Number}} **/ var INHERIT = -1; /** * DefaultsActions class. */ var DefaultsActions = function() { this.registerEvents(); }; /** * Register event listeners. */ DefaultsActions.prototype.registerEvents = function() { $(ACTIONS.EDIT_LEVEL_DEFAULTS).click(function(e) { e.preventDefault(); var button = $(this); var contextLevel = button.data('contextlevel'); var category = button.data('category'); var purpose = button.data('purpose'); // Get options. var requests = [ {methodname: 'tool_dataprivacy_get_category_options', args: {}}, {methodname: 'tool_dataprivacy_get_purpose_options', args: {}} ]; var promises = Ajax.call(requests); var titlePromise = Str.get_string('editdefaults', 'tool_dataprivacy', $('#defaults-header').text()); $.when(promises[0], promises[1], titlePromise).then(function(categoryResponse, purposeResponse, title) { var categories = categoryResponse.options; var purposes = purposeResponse.options; showDefaultsFormModal(title, contextLevel, category, purpose, null, categories, purposes, null); return true; }).catch(Notification.exception); }); $(ACTIONS.NEW_ACTIVITY_DEFAULTS).click(function(e) { e.preventDefault(); var button = $(this); var contextLevel = button.data('contextlevel'); // Get options. var requests = [ {methodname: 'tool_dataprivacy_get_category_options', args: {}}, {methodname: 'tool_dataprivacy_get_purpose_options', args: {}}, {methodname: 'tool_dataprivacy_get_activity_options', args: {'nodefaults': true}} ]; var promises = Ajax.call(requests); var titlePromise = Str.get_string('addnewdefaults', 'tool_dataprivacy'); $.when(promises[0], promises[1], promises[2], titlePromise).then( function(categoryResponse, purposeResponse, activityResponse, title) { var categories = categoryResponse.options; var purposes = purposeResponse.options; var activities = activityResponse.options; showDefaultsFormModal(title, contextLevel, null, null, null, categories, purposes, activities); return true; }).catch(Notification.exception); } ); $(ACTIONS.EDIT_ACTIVITY_DEFAULTS).click(function(e) { e.preventDefault(); var button = $(this); var contextLevel = button.data('contextlevel'); var category = button.data('category'); var purpose = button.data('purpose'); var activity = button.data('activityname'); // Get options. var requests = [ {methodname: 'tool_dataprivacy_get_category_options', args: {}}, {methodname: 'tool_dataprivacy_get_purpose_options', args: {}}, {methodname: 'tool_dataprivacy_get_activity_options', args: {}} ]; var promises = Ajax.call(requests); var titlePromise = Str.get_string('editmoduledefaults', 'tool_dataprivacy'); $.when(promises[0], promises[1], promises[2], titlePromise).then( function(categoryResponse, purposeResponse, activityResponse, title) { var categories = categoryResponse.options; var purposes = purposeResponse.options; var activities = activityResponse.options; showDefaultsFormModal(title, contextLevel, category, purpose, activity, categories, purposes, activities); return true; }).catch(Notification.exception); } ); $(ACTIONS.DELETE_ACTIVITY_DEFAULTS).click(function(e) { e.preventDefault(); var button = $(this); var contextLevel = button.data('contextlevel'); var activity = button.data('activityname'); var activityDisplayName = button.data('activitydisplayname'); // Set category and purpose to inherit (-1). var category = INHERIT; var purpose = INHERIT; ModalFactory.create({ title: Str.get_string('deletedefaults', 'tool_dataprivacy', activityDisplayName), body: Templates.render('tool_dataprivacy/delete_activity_defaults', {"activityname": activityDisplayName}), type: ModalFactory.types.SAVE_CANCEL, large: true }).then(function(modal) { modal.setSaveButtonText(Str.get_string('delete')); // Handle save event. modal.getRoot().on(ModalEvents.save, function() { setContextDefaults(contextLevel, category, purpose, activity, false); }); // Handle hidden event. modal.getRoot().on(ModalEvents.hidden, function() { // Destroy when hidden. modal.destroy(); }); modal.show(); return true; }).catch(Notification.exception); }); }; /** * Prepares and renders the modal for setting the defaults for the given context level/plugin. * * @param {String} title The modal's title. * @param {Number} contextLevel The context level to set defaults for. * @param {Number} category The current category ID. * @param {Number} purpose The current purpose ID. * @param {String} activity The plugin name of the activity. Optional. * @param {Array} categoryOptions The list of category options. * @param {Array} purposeOptions The list of purpose options. * @param {Array} activityOptions The list of activity options. Optional. */ function showDefaultsFormModal(title, contextLevel, category, purpose, activity, categoryOptions, purposeOptions, activityOptions) { if (category !== null) { categoryOptions.forEach(function(currentValue) { if (currentValue.id === category) { currentValue.selected = true; } }); } if (purpose !== null) { purposeOptions.forEach(function(currentValue) { if (currentValue.id === purpose) { currentValue.selected = true; } }); } var templateContext = { "contextlevel": contextLevel, "categoryoptions": categoryOptions, "purposeoptions": purposeOptions }; // Check the activityOptions parameter that was passed. if (activityOptions !== null && activityOptions.length) { // Check the activity parameter that was passed. if (activity === null) { // We're setting a new defaults for a module. templateContext.newactivitydefaults = true; } else { // Edit mode. Set selection. activityOptions.forEach(function(currentValue) { if (activity === currentValue.name) { currentValue.selected = true; } }); } templateContext.modemodule = true; templateContext.activityoptions = activityOptions; } ModalFactory.create({ title: title, body: Templates.render('tool_dataprivacy/category_purpose_form', templateContext), type: ModalFactory.types.SAVE_CANCEL, large: true }).then(function(modal) { // Handle save event. modal.getRoot().on(ModalEvents.save, function() { var activity = $('#activity'); var activityVal = typeof activity !== 'undefined' ? activity.val() : null; var override = $('#override'); var overrideVal = typeof override !== 'undefined' ? override.is(':checked') : false; setContextDefaults($('#contextlevel').val(), $('#category').val(), $('#purpose').val(), activityVal, overrideVal); }); // Handle hidden event. modal.getRoot().on(ModalEvents.hidden, function() { // Destroy when hidden. modal.destroy(); }); modal.show(); return modal; }).catch(Notification.exception); } /** * Calls a the tool_dataprivacy_set_context_defaults WS function. * * @param {Number} contextLevel The context level. * @param {Number} category The category ID. * @param {Number} purpose The purpose ID. * @param {String} activity The plugin name of the activity module. * @param {Boolean} override Whether to override custom instances. */ function setContextDefaults(contextLevel, category, purpose, activity, override) { var request = { methodname: 'tool_dataprivacy_set_context_defaults', args: { 'contextlevel': contextLevel, 'category': category, 'purpose': purpose, 'override': override, 'activity': activity } }; Ajax.call([request])[0].done(function(data) { if (data.result) { window.location.reload(); } }); } return /** @alias module:tool_dataprivacy/defaultsactions */ { // Public variables and functions. /** * Initialise the module. * * @method init * @return {DefaultsActions} */ 'init': function() { return new DefaultsActions(); } }; }); amd/src/myrequestactions.js 0000644 00000005136 15152701722 0012060 0 ustar 00 // 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/>. /** * AMD module to enable users to manage their own data requests. * * @module tool_dataprivacy/myrequestactions * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import Ajax from 'core/ajax'; import Notification from 'core/notification'; import Pending from 'core/pending'; import {get_strings as getStrings} from 'core/str'; const SELECTORS = { CANCEL_REQUEST: '[data-action="cancel"][data-requestid]', }; /** * Initialize module */ export const init = () => { document.addEventListener('click', event => { const triggerElement = event.target.closest(SELECTORS.CANCEL_REQUEST); if (triggerElement === null) { return; } event.preventDefault(); const requiredStrings = [ {key: 'cancelrequest', component: 'tool_dataprivacy'}, {key: 'cancelrequestconfirmation', component: 'tool_dataprivacy'}, ]; getStrings(requiredStrings).then(([cancelRequest, cancelConfirm]) => { return Notification.confirm(cancelRequest, cancelConfirm, cancelRequest, null, () => { const pendingPromise = new Pending('tool/dataprivacy:cancelRequest'); const request = { methodname: 'tool_dataprivacy_cancel_data_request', args: {requestid: triggerElement.dataset.requestid} }; Ajax.call([request])[0].then(response => { if (response.result) { window.location.reload(); } else { Notification.addNotification({ type: 'error', message: response.warnings[0].message }); } return pendingPromise.resolve(); }).catch(Notification.exception); }); }).catch(); }); }; amd/src/effective_retention_period.js 0000644 00000005354 15152701722 0014034 0 ustar 00 // 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/>. /** * Module to update the displayed retention period. * * @module tool_dataprivacy/effective_retention_period * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define(['jquery'], function($) { var SELECTORS = { PURPOSE_SELECT: '#id_purposeid', RETENTION_FIELD: '#fitem_id_retention_current [data-fieldtype=static]', }; /** * Constructor for the retention period display. * * @param {Array} purposeRetentionPeriods Associative array of purposeids with effective retention period at this context */ var EffectiveRetentionPeriod = function(purposeRetentionPeriods) { this.purposeRetentionPeriods = purposeRetentionPeriods; this.registerEventListeners(); }; /** * Removes the current 'change' listeners. * * Useful when a new form is loaded. */ var removeListeners = function() { $(SELECTORS.PURPOSE_SELECT).off('change'); }; /** * @var {Array} purposeRetentionPeriods * @private */ EffectiveRetentionPeriod.prototype.purposeRetentionPeriods = []; /** * Add purpose change listeners. * * @method registerEventListeners */ EffectiveRetentionPeriod.prototype.registerEventListeners = function() { $(SELECTORS.PURPOSE_SELECT).on('change', function(ev) { var selected = $(ev.currentTarget).val(); var selectedPurpose = this.purposeRetentionPeriods[selected]; $(SELECTORS.RETENTION_FIELD).text(selectedPurpose); }.bind(this)); }; return /** @alias module:tool_dataprivacy/effective_retention_period */ { init: function(purposeRetentionPeriods) { // Remove previously attached listeners. removeListeners(); return new EffectiveRetentionPeriod(purposeRetentionPeriods); } }; } ); amd/src/purposesactions.js 0000644 00000010130 15152701722 0011670 0 ustar 00 // 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/>. /** * AMD module for purposes actions. * * @module tool_dataprivacy/purposesactions * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define([ 'jquery', 'core/ajax', 'core/notification', 'core/str', 'core/modal_factory', 'core/modal_events'], function($, Ajax, Notification, Str, ModalFactory, ModalEvents) { /** * List of action selectors. * * @type {{DELETE: string}} */ var ACTIONS = { DELETE: '[data-action="deletepurpose"]', }; /** * PurposesActions class. */ var PurposesActions = function() { this.registerEvents(); }; /** * Register event listeners. */ PurposesActions.prototype.registerEvents = function() { $(ACTIONS.DELETE).click(function(e) { e.preventDefault(); var id = $(this).data('id'); var purposename = $(this).data('name'); var stringkeys = [ { key: 'deletepurpose', component: 'tool_dataprivacy' }, { key: 'deletepurposetext', component: 'tool_dataprivacy', param: purposename }, { key: 'delete' } ]; Str.get_strings(stringkeys).then(function(langStrings) { var title = langStrings[0]; var confirmMessage = langStrings[1]; var buttonText = langStrings[2]; return ModalFactory.create({ title: title, body: confirmMessage, type: ModalFactory.types.SAVE_CANCEL }).then(function(modal) { modal.setSaveButtonText(buttonText); // Handle save event. modal.getRoot().on(ModalEvents.save, function() { var request = { methodname: 'tool_dataprivacy_delete_purpose', args: {'id': id} }; Ajax.call([request])[0].done(function(data) { if (data.result) { $('tr[data-purposeid="' + id + '"]').remove(); } else { Notification.addNotification({ message: data.warnings[0].message, type: 'error' }); } }).fail(Notification.exception); }); // Handle hidden event. modal.getRoot().on(ModalEvents.hidden, function() { // Destroy when hidden. modal.destroy(); }); return modal; }); }).done(function(modal) { modal.show(); }).fail(Notification.exception); }); }; return /** @alias module:tool_dataprivacy/purposesactions */ { // Public variables and functions. /** * Initialise the module. * * @method init * @return {PurposesActions} */ 'init': function() { return new PurposesActions(); } }; }); amd/src/contactdpo.js 0000644 00000004465 15152701722 0010603 0 ustar 00 // 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/>. /** * Javascript module for contacting the site DPO * * @module tool_dataprivacy/contactdpo * @copyright 2021 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import ModalForm from 'core_form/modalform'; import Notification from 'core/notification'; import {get_string as getString} from 'core/str'; import {add as addToast} from 'core/toast'; const SELECTORS = { CONTACT_DPO: '[data-action="contactdpo"]', }; /** * Initialize module */ export const init = () => { const triggerElement = document.querySelector(SELECTORS.CONTACT_DPO); triggerElement.addEventListener('click', event => { event.preventDefault(); const modalForm = new ModalForm({ modalConfig: { title: getString('contactdataprotectionofficer', 'tool_dataprivacy'), }, formClass: 'tool_dataprivacy\\form\\contactdpo', saveButtonText: getString('send', 'tool_dataprivacy'), returnFocus: triggerElement, }); // Show a toast notification when the form is submitted. modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, event => { if (event.detail.result) { getString('requestsubmitted', 'tool_dataprivacy').then(addToast).catch(); } else { const warningMessages = event.detail.warnings.map(warning => warning.message); Notification.addNotification({ type: 'error', message: warningMessages.join('<br>') }); } }); modalForm.show(); }); }; amd/src/add_category.js 0000644 00000013451 15152701722 0011065 0 ustar 00 // 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/>. /** * Module to add categories. * * @module tool_dataprivacy/add_category * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define([ 'jquery', 'core/str', 'core/ajax', 'core/notification', 'core/modal_factory', 'core/modal_events', 'core/fragment', 'core_form/changechecker', ], function( $, Str, Ajax, Notification, ModalFactory, ModalEvents, Fragment, FormChangeChecker ) { var SELECTORS = { CATEGORY_LINK: '[data-add-element="category"]', }; var AddCategory = function(contextId) { this.contextId = contextId; var stringKeys = [ { key: 'addcategory', component: 'tool_dataprivacy' }, { key: 'save', component: 'admin' } ]; this.strings = Str.get_strings(stringKeys); this.registerEventListeners(); }; /** * @var {int} contextId * @private */ AddCategory.prototype.contextId = 0; /** * @var {Promise} * @private */ AddCategory.prototype.strings = 0; AddCategory.prototype.registerEventListeners = function() { var trigger = $(SELECTORS.CATEGORY_LINK); trigger.on('click', function() { return this.strings.then(function(strings) { ModalFactory.create({ type: ModalFactory.types.SAVE_CANCEL, title: strings[0], body: '', }, trigger).done(function(modal) { this.setupFormModal(modal, strings[1]); }.bind(this)); }.bind(this)) .fail(Notification.exception); }.bind(this)); }; /** * @method getBody * @param {Object} formdata * @private * @return {Promise} */ AddCategory.prototype.getBody = function(formdata) { var params = null; if (typeof formdata !== "undefined") { params = {jsonformdata: JSON.stringify(formdata)}; } // Get the content of the modal. return Fragment.loadFragment('tool_dataprivacy', 'addcategory_form', this.contextId, params); }; AddCategory.prototype.setupFormModal = function(modal, saveText) { modal.setLarge(); modal.setSaveButtonText(saveText); // We want to reset the form every time it is opened. modal.getRoot().on(ModalEvents.hidden, this.destroy.bind(this)); modal.setBody(this.getBody()); // We catch the modal save event, and use it to submit the form inside the modal. // Triggering a form submission will give JS validation scripts a chance to check for errors. modal.getRoot().on(ModalEvents.save, this.submitForm.bind(this)); // We also catch the form submit event and use it to submit the form with ajax. modal.getRoot().on('submit', 'form', this.submitFormAjax.bind(this)); this.modal = modal; modal.show(); }; /** * This triggers a form submission, so that any mform elements can do final tricks before the form submission is processed. * * @method submitForm * @param {Event} e Form submission event. * @private */ AddCategory.prototype.submitForm = function(e) { e.preventDefault(); this.modal.getRoot().find('form').submit(); }; AddCategory.prototype.submitFormAjax = function(e) { // We don't want to do a real form submission. e.preventDefault(); // Convert all the form elements values to a serialised string. var formData = this.modal.getRoot().find('form').serialize(); Ajax.call([{ methodname: 'tool_dataprivacy_create_category_form', args: {jsonformdata: JSON.stringify(formData)}, done: function(data) { if (data.validationerrors) { this.modal.setBody(this.getBody(formData)); } else { this.close(); } }.bind(this), fail: Notification.exception }]); }; AddCategory.prototype.close = function() { this.destroy(); document.location.reload(); }; AddCategory.prototype.destroy = function() { FormChangeChecker.resetAllFormDirtyStates(); this.modal.destroy(); }; AddCategory.prototype.removeListeners = function() { $(SELECTORS.CATEGORY_LINK).off('click'); }; return /** @alias module:tool_dataprivacy/add_category */ { getInstance: function(contextId) { return new AddCategory(contextId); } }; } ); amd/src/expand_contract.js 0000644 00000007740 15152701722 0011620 0 ustar 00 // 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/>. /** * Potential user selector module. * * @module tool_dataprivacy/expand_contract * @copyright 2018 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define(['jquery', 'core/url', 'core/str', 'core/notification'], function($, url, str, Notification) { var expandedImage = $('<img alt="" src="' + url.imageUrl('t/expanded') + '"/>'); var collapsedImage = $('<img alt="" src="' + url.imageUrl('t/collapsed') + '"/>'); /* * Class names to apply when expanding/collapsing nodes. */ var CLASSES = { EXPAND: 'fa-caret-right', COLLAPSE: 'fa-caret-down' }; return /** @alias module:tool_dataprivacy/expand-collapse */ { /** * Expand or collapse a selected node. * * @param {object} targetnode The node that we want to expand / collapse * @param {object} thisnode The node that was clicked. */ expandCollapse: function(targetnode, thisnode) { if (targetnode.hasClass('hide')) { targetnode.removeClass('hide'); targetnode.addClass('visible'); targetnode.attr('aria-expanded', true); thisnode.find(':header i.fa').removeClass(CLASSES.EXPAND); thisnode.find(':header i.fa').addClass(CLASSES.COLLAPSE); thisnode.find(':header img.icon').attr('src', expandedImage.attr('src')); } else { targetnode.removeClass('visible'); targetnode.addClass('hide'); targetnode.attr('aria-expanded', false); thisnode.find(':header i.fa').removeClass(CLASSES.COLLAPSE); thisnode.find(':header i.fa').addClass(CLASSES.EXPAND); thisnode.find(':header img.icon').attr('src', collapsedImage.attr('src')); } }, /** * Expand or collapse all nodes on this page. * * @param {string} nextstate The next state to change to. */ expandCollapseAll: function(nextstate) { var currentstate = (nextstate == 'visible') ? 'hide' : 'visible'; var ariaexpandedstate = (nextstate == 'visible') ? true : false; var iconclassnow = (nextstate == 'visible') ? CLASSES.EXPAND : CLASSES.COLLAPSE; var iconclassnext = (nextstate == 'visible') ? CLASSES.COLLAPSE : CLASSES.EXPAND; var imagenow = (nextstate == 'visible') ? expandedImage.attr('src') : collapsedImage.attr('src'); $('.' + currentstate).each(function() { $(this).removeClass(currentstate); $(this).addClass(nextstate); $(this).attr('aria-expanded', ariaexpandedstate); }); $('.tool_dataprivacy-expand-all').data('visibilityState', currentstate); str.get_string(currentstate, 'tool_dataprivacy').then(function(langString) { $('.tool_dataprivacy-expand-all').html(langString); return; }).catch(Notification.exception); $(':header i.fa').each(function() { $(this).removeClass(iconclassnow); $(this).addClass(iconclassnext); }); $(':header img.icon').each(function() { $(this).attr('src', imagenow); }); } }; }); amd/src/form-user-selector.js 0000644 00000004574 15152701722 0012203 0 ustar 00 // 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/>. /** * Potential user selector module. * * @module tool_dataprivacy/form-user-selector * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define(['jquery', 'core/ajax', 'core/templates'], function($, Ajax, Templates) { return /** @alias module:tool_dataprivacy/form-user-selector */ { processResults: function(selector, results) { var users = []; $.each(results, function(index, user) { users.push({ value: user.id, label: user._label }); }); return users; }, transport: function(selector, query, success, failure) { var promise; promise = Ajax.call([{ methodname: 'tool_dataprivacy_get_users', args: { query: query } }]); promise[0].then(function(results) { var promises = [], i = 0; // Render the label. $.each(results, function(index, user) { promises.push(Templates.render('tool_dataprivacy/form-user-selector-suggestion', user)); }); // Apply the label to the results. return $.when.apply($.when, promises).then(function() { var args = arguments; $.each(results, function(index, user) { user._label = args[i]; i++; }); success(results); return; }); }).fail(failure); } }; }); amd/src/events.js 0000644 00000002326 15152701722 0007743 0 ustar 00 // 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/>. /** * Contain the events the data privacy tool can fire. * * @module tool_dataprivacy/events * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define([], function() { return { approve: 'tool_dataprivacy-data_request:approve', bulkApprove: 'tool_dataprivacy-data_request:bulk_approve', deny: 'tool_dataprivacy-data_request:deny', bulkDeny: 'tool_dataprivacy-data_request:bulk_deny', complete: 'tool_dataprivacy-data_request:complete' }; }); amd/src/categoriesactions.js 0000644 00000010157 15152701722 0012146 0 ustar 00 // 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/>. /** * AMD module for categories actions. * * @module tool_dataprivacy/categoriesactions * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define([ 'jquery', 'core/ajax', 'core/notification', 'core/str', 'core/modal_factory', 'core/modal_events'], function($, Ajax, Notification, Str, ModalFactory, ModalEvents) { /** * List of action selectors. * * @type {{DELETE: string}} */ var ACTIONS = { DELETE: '[data-action="deletecategory"]', }; /** * CategoriesActions class. */ var CategoriesActions = function() { this.registerEvents(); }; /** * Register event listeners. */ CategoriesActions.prototype.registerEvents = function() { $(ACTIONS.DELETE).click(function(e) { e.preventDefault(); var id = $(this).data('id'); var categoryname = $(this).data('name'); var stringkeys = [ { key: 'deletecategory', component: 'tool_dataprivacy' }, { key: 'deletecategorytext', component: 'tool_dataprivacy', param: categoryname }, { key: 'delete' } ]; Str.get_strings(stringkeys).then(function(langStrings) { var title = langStrings[0]; var confirmMessage = langStrings[1]; var buttonText = langStrings[2]; return ModalFactory.create({ title: title, body: confirmMessage, type: ModalFactory.types.SAVE_CANCEL }).then(function(modal) { modal.setSaveButtonText(buttonText); // Handle save event. modal.getRoot().on(ModalEvents.save, function() { var request = { methodname: 'tool_dataprivacy_delete_category', args: {'id': id} }; Ajax.call([request])[0].done(function(data) { if (data.result) { $('tr[data-categoryid="' + id + '"]').remove(); } else { Notification.addNotification({ message: data.warnings[0].message, type: 'error' }); } }).fail(Notification.exception); }); // Handle hidden event. modal.getRoot().on(ModalEvents.hidden, function() { // Destroy when hidden. modal.destroy(); }); return modal; }); }).done(function(modal) { modal.show(); }).fail(Notification.exception); }); }; return /** @alias module:tool_dataprivacy/categoriesactions */ { // Public variables and functions. /** * Initialise the module. * * @method init * @return {CategoriesActions} */ 'init': function() { return new CategoriesActions(); } }; }); pluginregistry.php 0000644 00000003775 15152701722 0010362 0 ustar 00 <?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/>. /** * Prints the compliance data registry main page. * * @copyright 2018 onwards Adrian Greeve <adriangreeve.com> * @license http://www.gnu.org/copyleft/gpl.html GNU Public License * @package tool_dataprivacy */ require_once(__DIR__ . '/../../../config.php'); require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php'); require_login(null, false); $contextlevel = optional_param('contextlevel', CONTEXT_SYSTEM, PARAM_INT); $contextid = optional_param('contextid', 0, PARAM_INT); $url = new moodle_url('/' . $CFG->admin . '/tool/dataprivacy/pluginregistry.php'); $title = get_string('pluginregistry', 'tool_dataprivacy'); \tool_dataprivacy\page_helper::setup($url, $title); $output = $PAGE->get_renderer('tool_dataprivacy'); echo $output->header(); if (\tool_dataprivacy\api::is_site_dpo($USER->id)) { // Get data! $metadatatool = new \tool_dataprivacy\metadata_registry(); $metadata = $metadatatool->get_registry_metadata(); $dataregistry = new tool_dataprivacy\output\data_registry_compliance_page($metadata); echo $output->render($dataregistry); } else { $dponamestring = implode (', ', tool_dataprivacy\api::get_dpo_role_names()); $message = get_string('privacyofficeronly', 'tool_dataprivacy', $dponamestring); echo $OUTPUT->notification($message, 'error'); } echo $OUTPUT->footer(); tests/data_request_test.php 0000644 00000017202 15152701722 0012143 0 ustar 00 <?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/>. namespace tool_dataprivacy; use data_privacy_testcase; defined('MOODLE_INTERNAL') || die(); require_once('data_privacy_testcase.php'); /** * Tests for the data_request persistent. * * @package tool_dataprivacy * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class data_request_test extends data_privacy_testcase { /** * Data provider for testing is_resettable, and is_active. * * @return array */ public function status_state_provider() : array { return [ [ 'state' => api::DATAREQUEST_STATUS_PENDING, 'resettable' => false, 'active' => false, ], [ 'state' => api::DATAREQUEST_STATUS_AWAITING_APPROVAL, 'resettable' => false, 'active' => false, ], [ 'state' => api::DATAREQUEST_STATUS_APPROVED, 'resettable' => true, 'active' => true, ], [ 'state' => api::DATAREQUEST_STATUS_PROCESSING, 'resettable' => false, 'active' => false, ], [ 'state' => api::DATAREQUEST_STATUS_COMPLETE, 'resettable' => false, 'active' => false, ], [ 'state' => api::DATAREQUEST_STATUS_CANCELLED, 'resettable' => false, 'active' => false, ], [ 'state' => api::DATAREQUEST_STATUS_REJECTED, 'resettable' => true, 'active' => false, ], [ 'state' => api::DATAREQUEST_STATUS_DOWNLOAD_READY, 'resettable' => false, 'active' => false, ], [ 'state' => api::DATAREQUEST_STATUS_EXPIRED, 'resettable' => false, 'active' => false, ], ]; } /** * Test the pseudo states of a data request with an export request. * * @dataProvider status_state_provider * @param int $status * @param bool $resettable * @param bool $active */ public function test_pseudo_states_export(int $status, bool $resettable, bool $active) { $uut = new \tool_dataprivacy\data_request(); $uut->set('status', $status); $uut->set('type', api::DATAREQUEST_TYPE_EXPORT); $this->assertEquals($resettable, $uut->is_resettable()); $this->assertEquals($active, $uut->is_active()); } /** * Test the pseudo states of a data request with a delete request. * * @dataProvider status_state_provider * @param int $status * @param bool $resettable * @param bool $active */ public function test_pseudo_states_delete(int $status, bool $resettable, bool $active) { $uut = new \tool_dataprivacy\data_request(); $uut->set('status', $status); $uut->set('type', api::DATAREQUEST_TYPE_DELETE); $this->assertEquals($resettable, $uut->is_resettable()); $this->assertEquals($active, $uut->is_active()); } /** * Test the pseudo states of a data request. * * @dataProvider status_state_provider * @param int $status */ public function test_can_reset_others($status) { $uut = new \tool_dataprivacy\data_request(); $uut->set('status', $status); $uut->set('type', api::DATAREQUEST_TYPE_OTHERS); $this->assertFalse($uut->is_resettable()); } /** * Data provider for states which are not resettable. * * @return array */ public function non_resettable_provider() : array { $states = []; foreach ($this->status_state_provider() as $thisstatus) { if (!$thisstatus['resettable']) { $states[] = $thisstatus; } } return $states; } /** * Ensure that requests which are not resettable cause an exception to be thrown. * * @dataProvider non_resettable_provider * @param int $status */ public function test_non_resubmit_request($status) { $uut = new \tool_dataprivacy\data_request(); $uut->set('status', $status); $this->expectException(\moodle_exception::class); $this->expectExceptionMessage(get_string('cannotreset', 'tool_dataprivacy')); $uut->resubmit_request(); } /** * Ensure that a rejected request can be reset. */ public function test_resubmit_request() { $this->resetAfterTest(); $uut = new \tool_dataprivacy\data_request(); $uut->set('status', api::DATAREQUEST_STATUS_REJECTED); $uut->set('type', api::DATAREQUEST_TYPE_DELETE); $uut->set('comments', 'Foo'); $uut->set('requestedby', 42); $uut->set('dpo', 98); $newrequest = $uut->resubmit_request(); $this->assertEquals('Foo', $newrequest->get('comments')); $this->assertEquals(42, $newrequest->get('requestedby')); $this->assertEquals(98, $newrequest->get('dpo')); $this->assertEquals(api::DATAREQUEST_STATUS_AWAITING_APPROVAL, $newrequest->get('status')); $this->assertEquals(api::DATAREQUEST_TYPE_DELETE, $newrequest->get('type')); $this->assertEquals(api::DATAREQUEST_STATUS_REJECTED, $uut->get('status')); } /** * Ensure that an active request can be reset. */ public function test_resubmit_active_request() { $this->resetAfterTest(); $uut = new \tool_dataprivacy\data_request(); $uut->set('status', api::DATAREQUEST_STATUS_APPROVED); $uut->set('type', api::DATAREQUEST_TYPE_DELETE); $uut->set('comments', 'Foo'); $uut->set('requestedby', 42); $uut->set('dpo', 98); $newrequest = $uut->resubmit_request(); $this->assertEquals('Foo', $newrequest->get('comments')); $this->assertEquals(42, $newrequest->get('requestedby')); $this->assertEquals(98, $newrequest->get('dpo')); $this->assertEquals(api::DATAREQUEST_STATUS_AWAITING_APPROVAL, $newrequest->get('status')); $this->assertEquals(api::DATAREQUEST_TYPE_DELETE, $newrequest->get('type')); $this->assertEquals(api::DATAREQUEST_STATUS_REJECTED, $uut->get('status')); } /** * Create a data request for the user. * * @param int $userid * @param int $type * @param int $status * @return data_request */ public function create_request_for_user_with_status(int $userid, int $type, int $status) : data_request { $request = new data_request(0, (object) [ 'userid' => $userid, 'type' => $type, 'status' => $status, ]); $request->save(); return $request; } } tests/privacy/provider_test.php 0000644 00000015137 15152701722 0012776 0 ustar 00 <?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/>. /** * Tests for the plugin privacy provider * * @package tool_dataprivacy * @copyright 2020 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\privacy; defined('MOODLE_INTERNAL') || die(); use core_privacy\local\request\userlist; use core_privacy\local\request\writer; use core_privacy\tests\provider_testcase; use tool_dataprivacy\api; use tool_dataprivacy\local\helper; use tool_dataprivacy\privacy\provider; /** * Privacy provider tests * * @package tool_dataprivacy * @copyright 2020 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider_test extends provider_testcase { /** * Test provider get_contexts_for_userid method * * @return void */ public function test_get_contexts_for_userid() { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $context = \context_user::instance($user->id); // Returned context list should contain a single item. $contextlist = $this->get_contexts_for_userid($user->id, 'tool_dataprivacy'); $this->assertCount(1, $contextlist); // We should have the user context of our test user. $this->assertSame($context, $contextlist->current()); } /** * Test provider get_users_in_context method * * @return void */ public function test_get_users_in_context() { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $context = \context_user::instance($user->id); $userlist = new userlist($context, 'tool_dataprivacy'); provider::get_users_in_context($userlist); $this->assertEquals([$user->id], $userlist->get_userids()); } /** * Test provider get_users_in_context method for a non-user context * * @return void */ public function test_get_users_in_context_non_user_context() { $context = \context_system::instance(); $userlist = new userlist($context, 'tool_dataprivacy'); provider::get_users_in_context($userlist); $this->assertEmpty($userlist); } /** * Test provider export_user_data method * * @return void */ public function test_export_user_data() { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $context = \context_user::instance($user->id); $this->setUser($user); // Create an export request, approve it. $requestexport = api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT, 'Please export my stuff'); api::update_request_status($requestexport->get('id'), api::DATAREQUEST_STATUS_APPROVED); // Create a deletion request, reject it. $requestdelete = api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE); api::update_request_status($requestdelete->get('id'), api::DATAREQUEST_STATUS_REJECTED, 0, 'Nope'); $this->export_context_data_for_user($user->id, $context, 'tool_dataprivacy'); /** @var \core_privacy\tests\request\content_writer $writer */ $writer = writer::with_context($context); $this->assertTrue($writer->has_any_data()); /** @var stdClass[] $data */ $data = (array) $writer->get_data([ get_string('privacyandpolicies', 'admin'), get_string('datarequests', 'tool_dataprivacy'), ]); $this->assertCount(2, $data); $strs = get_strings(['requesttypeexportshort', 'requesttypedeleteshort', 'statusapproved', 'statusrejected', 'creationmanual'], 'tool_dataprivacy'); // First item is the approved export request. $this->assertEquals($strs->requesttypeexportshort, $data[0]->type); $this->assertEquals($strs->statusapproved, $data[0]->status); $this->assertEquals($strs->creationmanual, $data[0]->creationmethod); $this->assertEquals($requestexport->get('comments'), $data[0]->comments); $this->assertEmpty($data[0]->dpocomment); $this->assertNotEmpty($data[0]->timecreated); // Next is the rejected deletion request. $this->assertEquals($strs->requesttypedeleteshort, $data[1]->type); $this->assertEquals($strs->statusrejected, $data[1]->status); $this->assertEquals($strs->creationmanual, $data[1]->creationmethod); $this->assertEmpty($data[1]->comments); $this->assertStringContainsString('Nope', $data[1]->dpocomment); $this->assertNotEmpty($data[1]->timecreated); } /** * Test class export_user_preferences method * * @return void */ public function test_export_user_preferences() { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); // Set filters preference. $filters = [ helper::FILTER_TYPE . ':' . api::DATAREQUEST_TYPE_EXPORT, helper::FILTER_STATUS . ':' . api::DATAREQUEST_STATUS_PENDING, ]; set_user_preference(helper::PREF_REQUEST_FILTERS, json_encode($filters), $user); // Set paging preference. set_user_preference(helper::PREF_REQUEST_PERPAGE, 6, $user); provider::export_user_preferences($user->id); /** @var \core_privacy\tests\request\content_writer $writer */ $writer = writer::with_context(\context_system::instance()); $this->assertTrue($writer->has_any_data()); /** @var stdClass[] $preferences */ $preferences = (array) $writer->get_user_preferences('tool_dataprivacy'); $this->assertCount(2, $preferences); $this->assertEquals((object) [ 'value' => '1:1, 2:0', 'description' => 'Type: Export, Status: Pending', ], $preferences[helper::PREF_REQUEST_FILTERS]); $this->assertEquals(6, $preferences[helper::PREF_REQUEST_PERPAGE]->value); } } tests/expired_data_requests_test.php 0000644 00000015644 15152701722 0014056 0 ustar 00 <?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/>. namespace tool_dataprivacy; use data_privacy_testcase; defined('MOODLE_INTERNAL') || die(); require_once('data_privacy_testcase.php'); /** * Expired data requests tests. * * @package tool_dataprivacy * @copyright 2018 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class expired_data_requests_test extends data_privacy_testcase { /** * Test tearDown. */ public function tearDown(): void { \core_privacy\local\request\writer::reset(); } /** * Test finding and deleting expired data requests */ public function test_data_request_expiry() { global $DB; $this->resetAfterTest(); \core_privacy\local\request\writer::setup_real_writer_instance(); // Set up test users. $this->setAdminUser(); $studentuser = $this->getDataGenerator()->create_user(); $studentusercontext = \context_user::instance($studentuser->id); $dpouser = $this->getDataGenerator()->create_user(); $this->assign_site_dpo($dpouser); // Set site purpose. $this->create_system_purpose(); // Set request expiry to 5 minutes. set_config('privacyrequestexpiry', 300, 'tool_dataprivacy'); // Create and approve data request. $this->setUser($studentuser->id); $datarequest = api::create_data_request($studentuser->id, api::DATAREQUEST_TYPE_EXPORT); $requestid = $datarequest->get('id'); $this->setAdminUser(); api::approve_data_request($requestid); $this->setUser(); ob_start(); $this->runAdhocTasks('\tool_dataprivacy\task\process_data_request_task'); ob_end_clean(); // Confirm approved and exported. $request = new data_request($requestid); $this->assertEquals(api::DATAREQUEST_STATUS_DOWNLOAD_READY, $request->get('status')); $fileconditions = array( 'userid' => $studentuser->id, 'component' => 'tool_dataprivacy', 'filearea' => 'export', 'itemid' => $requestid, 'contextid' => $studentusercontext->id, ); $this->assertEquals(2, $DB->count_records('files', $fileconditions)); // Run expiry deletion - should not affect test export. $expiredrequests = data_request::get_expired_requests(); $this->assertEquals(0, count($expiredrequests)); data_request::expire($expiredrequests); // Confirm test export was not deleted. $request = new data_request($requestid); $this->assertEquals(api::DATAREQUEST_STATUS_DOWNLOAD_READY, $request->get('status')); $this->assertEquals(2, $DB->count_records('files', $fileconditions)); // Change request expiry to 1 second and allow it to elapse. set_config('privacyrequestexpiry', 1, 'tool_dataprivacy'); $this->waitForSecond(); // Re-run expiry deletion, confirm the request expires and export is deleted. $expiredrequests = data_request::get_expired_requests(); $this->assertEquals(1, count($expiredrequests)); data_request::expire($expiredrequests); $request = new data_request($requestid); $this->assertEquals(api::DATAREQUEST_STATUS_EXPIRED, $request->get('status')); $this->assertEquals(0, $DB->count_records('files', $fileconditions)); } /** * Test for \tool_dataprivacy\data_request::is_expired() * Tests for the expected request status to protect from false positive/negative, * then tests is_expired() is returning the expected response. */ public function test_is_expired() { $this->resetAfterTest(); \core_privacy\local\request\writer::setup_real_writer_instance(); // Set request expiry beyond this test. set_config('privacyrequestexpiry', 20, 'tool_dataprivacy'); $admin = get_admin(); $this->setAdminUser(); // Set site purpose. $this->create_system_purpose(); // Create export request. $datarequest = api::create_data_request($admin->id, api::DATAREQUEST_TYPE_EXPORT); $requestid = $datarequest->get('id'); // Approve the request. ob_start(); $this->setAdminUser(); api::approve_data_request($requestid); $this->runAdhocTasks('\tool_dataprivacy\task\process_data_request_task'); ob_end_clean(); // Test Download ready (not expired) response. $request = new data_request($requestid); $this->assertEquals(api::DATAREQUEST_STATUS_DOWNLOAD_READY, $request->get('status')); $result = data_request::is_expired($request); $this->assertFalse($result); // Let request expiry time lapse. set_config('privacyrequestexpiry', 1, 'tool_dataprivacy'); $this->waitForSecond(); // Test Download ready (time expired) response. $request = new data_request($requestid); $this->assertEquals(api::DATAREQUEST_STATUS_DOWNLOAD_READY, $request->get('status')); $result = data_request::is_expired($request); $this->assertTrue($result); // Run the expiry task to properly expire the request. ob_start(); $task = \core\task\manager::get_scheduled_task('\tool_dataprivacy\task\delete_expired_requests'); $task->execute(); ob_end_clean(); // Test Expired response status response. $request = new data_request($requestid); $this->assertEquals(api::DATAREQUEST_STATUS_EXPIRED, $request->get('status')); $result = data_request::is_expired($request); $this->assertTrue($result); } /** * Create a site (system context) purpose and category. * * @return void */ protected function create_system_purpose() { $purpose = new purpose(0, (object) [ 'name' => 'Test purpose ' . rand(1, 1000), 'retentionperiod' => 'P1D', 'lawfulbases' => 'gdpr_art_6_1_a', ]); $purpose->create(); $cat = new category(0, (object) ['name' => 'Test category']); $cat->create(); $record = (object) [ 'purposeid' => $purpose->get('id'), 'categoryid' => $cat->get('id'), 'contextlevel' => CONTEXT_SYSTEM, ]; api::set_contextlevel($record); } } tests/user_deleted_observer_test.php 0000644 00000021016 15152701722 0014033 0 ustar 00 <?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/>. namespace tool_dataprivacy; use tool_dataprivacy\event\user_deleted_observer; /** * Event observer test. * * @package tool_dataprivacy * @copyright 2018 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_deleted_observer_test extends \advanced_testcase { /** * Ensure that a delete data request is created upon user deletion. */ public function test_create_delete_data_request() { $this->resetAfterTest(); // Enable automatic creation of delete data requests. set_config('automaticdeletionrequests', 1, 'tool_dataprivacy'); // Create another user who is not a DPO. $user = $this->getDataGenerator()->create_user(); $event = $this->trigger_delete_user_event($user); user_deleted_observer::create_delete_data_request($event); // Validate that delete data request has been created. $this->assertTrue(api::has_ongoing_request($user->id, api::DATAREQUEST_TYPE_DELETE)); } /** * Ensure that a delete data request is not created upon user deletion if automatic creation of * delete data requests is disabled. */ public function test_create_delete_data_request_automatic_creation_disabled() { $this->resetAfterTest(); // Disable automatic creation of delete data requests. set_config('automaticdeletionrequests', 0, 'tool_dataprivacy'); // Create another user who is not a DPO. $user = $this->getDataGenerator()->create_user(); $event = $this->trigger_delete_user_event($user); user_deleted_observer::create_delete_data_request($event); // Validate that delete data request has been created. $this->assertFalse(api::has_ongoing_request($user->id, api::DATAREQUEST_TYPE_DELETE)); } /** * Ensure that a delete data request is being created upon user deletion * if an ongoing export data request (or any other except delete data request) for that user already exists. */ public function test_create_delete_data_request_export_data_request_preexists() { $this->resetAfterTest(); $this->setAdminUser(); // Enable automatic creation of delete data requests. set_config('automaticdeletionrequests', 1, 'tool_dataprivacy'); // Create another user who is not a DPO. $user = $this->getDataGenerator()->create_user(); // Create a delete data request for $user. api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT); // Validate that delete data request has been created. $this->assertTrue(api::has_ongoing_request($user->id, api::DATAREQUEST_TYPE_EXPORT)); $this->assertEquals(0, api::get_data_requests_count($user->id, [], [api::DATAREQUEST_TYPE_DELETE])); $event = $this->trigger_delete_user_event($user); user_deleted_observer::create_delete_data_request($event); // Validate that delete data request has been created. $this->assertEquals(1, api::get_data_requests_count($user->id, [], [api::DATAREQUEST_TYPE_DELETE])); } /** * Ensure that a delete data request is not being created upon user deletion * if an ongoing delete data request for that user already exists. */ public function test_create_delete_data_request_ongoing_delete_data_request_preexists() { $this->resetAfterTest(); $this->setAdminUser(); // Enable automatic creation of delete data requests. set_config('automaticdeletionrequests', 1, 'tool_dataprivacy'); // Create another user who is not a DPO. $user = $this->getDataGenerator()->create_user(); // Create a delete data request for $user. api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE); // Validate that delete data request has been created. $this->assertTrue(api::has_ongoing_request($user->id, api::DATAREQUEST_TYPE_DELETE)); $event = $this->trigger_delete_user_event($user); user_deleted_observer::create_delete_data_request($event); // Validate that additional delete data request has not been created. $this->assertEquals(1, api::get_data_requests_count($user->id, [], [api::DATAREQUEST_TYPE_DELETE])); } /** * Ensure that a delete data request is being created upon user deletion * if a finished delete data request (excluding complete) for that user already exists. */ public function test_create_delete_data_request_canceled_delete_data_request_preexists() { $this->resetAfterTest(); $this->setAdminUser(); // Enable automatic creation of delete data requests. set_config('automaticdeletionrequests', 1, 'tool_dataprivacy'); // Create another user who is not a DPO. $user = $this->getDataGenerator()->create_user(); // Create a delete data request for $user. $datarequest = api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE); $requestid = $datarequest->get('id'); api::update_request_status($requestid, api::DATAREQUEST_STATUS_CANCELLED); // Validate that delete data request has been created and the status has been updated to 'Canceled'. $this->assertEquals(1, api::get_data_requests_count($user->id, [], [api::DATAREQUEST_TYPE_DELETE])); $this->assertFalse(api::has_ongoing_request($user->id, api::DATAREQUEST_TYPE_DELETE)); $event = $this->trigger_delete_user_event($user); user_deleted_observer::create_delete_data_request($event); // Validate that additional delete data request has been created. $this->assertEquals(2, api::get_data_requests_count($user->id, [], [api::DATAREQUEST_TYPE_DELETE])); $this->assertTrue(api::has_ongoing_request($user->id, api::DATAREQUEST_TYPE_DELETE)); } /** * Ensure that a delete data request is being created upon user deletion * if a completed delete data request for that user already exists. */ public function test_create_delete_data_request_completed_delete_data_request_preexists() { $this->resetAfterTest(); $this->setAdminUser(); // Enable automatic creation of delete data requests. set_config('automaticdeletionrequests', 1, 'tool_dataprivacy'); // Create another user who is not a DPO. $user = $this->getDataGenerator()->create_user(); // Create a delete data request for $user. $datarequest = api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE); $requestid = $datarequest->get('id'); api::update_request_status($requestid, api::DATAREQUEST_STATUS_COMPLETE); // Validate that delete data request has been created and the status has been updated to 'Completed'. $this->assertEquals(1, api::get_data_requests_count($user->id, [], [api::DATAREQUEST_TYPE_DELETE])); $this->assertFalse(api::has_ongoing_request($user->id, api::DATAREQUEST_TYPE_DELETE)); $event = $this->trigger_delete_user_event($user); user_deleted_observer::create_delete_data_request($event); // Validate that additional delete data request has not been created. $this->assertEquals(1, api::get_data_requests_count($user->id, [], [api::DATAREQUEST_TYPE_DELETE])); $this->assertFalse(api::has_ongoing_request($user->id, api::DATAREQUEST_TYPE_DELETE)); } /** * Helper to trigger and capture the delete user event. * * @param object $user The user object. * @return \core\event\user_deleted $event The returned event. */ private function trigger_delete_user_event($user) { $sink = $this->redirectEvents(); delete_user($user); $events = $sink->get_events(); $sink->close(); $event = reset($events); // Validate event data. $this->assertInstanceOf('\core\event\user_deleted', $event); return $event; } } tests/manager_observer_test.php 0000644 00000006172 15152701722 0013007 0 ustar 00 <?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/>. namespace tool_dataprivacy; use data_privacy_testcase; defined('MOODLE_INTERNAL') || die(); require_once('data_privacy_testcase.php'); /** * Tests for the manager observer. * * @package tool_dataprivacy * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class manager_observer_test extends data_privacy_testcase { /** * Ensure that when users are configured as DPO, they are sent an message upon failure. */ public function test_handle_component_failure() { $this->resetAfterTest(); // Create another user who is not a DPO. $this->getDataGenerator()->create_user(); // Create two DPOs. $dpo1 = $this->getDataGenerator()->create_user(); $dpo2 = $this->getDataGenerator()->create_user(); $this->assign_site_dpo(array($dpo1, $dpo2)); $dpos = \tool_dataprivacy\api::get_site_dpos(); $observer = new \tool_dataprivacy\manager_observer(); // Handle the failure, catching messages. $mailsink = $this->redirectMessages(); $mailsink->clear(); $observer->handle_component_failure(new \Exception('error'), 'foo', 'bar', 'baz', ['foobarbaz', 'bum']); // Messages should be sent to both DPOs only. $this->assertEquals(2, $mailsink->count()); $messages = $mailsink->get_messages(); $messageusers = array_map(function($message) { return $message->useridto; }, $messages); $this->assertEqualsCanonicalizing(array_keys($dpos), $messageusers); } /** * Ensure that when no user is configured as DPO, the message is sent to admin instead. */ public function test_handle_component_failure_no_dpo() { $this->resetAfterTest(); // Create another user who is not a DPO or admin. $this->getDataGenerator()->create_user(); $observer = new \tool_dataprivacy\manager_observer(); $mailsink = $this->redirectMessages(); $mailsink->clear(); $observer->handle_component_failure(new \Exception('error'), 'foo', 'bar', 'baz', ['foobarbaz', 'bum']); // Messages should have been sent only to the admin. $this->assertEquals(1, $mailsink->count()); $messages = $mailsink->get_messages(); $message = reset($messages); $admin = \core_user::get_user_by_username('admin'); $this->assertEquals($admin->id, $message->useridto); } } tests/metadata_registry_test.php 0000644 00000007137 15152701722 0013200 0 ustar 00 <?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/>. namespace tool_dataprivacy; /** * Metadata registry tests. * * @package tool_dataprivacy * @copyright 2018 Adrian Greeve <adriangreeve.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class metadata_registry_test extends \advanced_testcase { /** * Fetch the meta data and return it in a form that we can easily unit test. * * @return array the meta data. */ protected function get_meta_data() { $metadataregistry = new \tool_dataprivacy\metadata_registry(); $data = $metadataregistry->get_registry_metadata(); $newdata = []; foreach ($data as $value) { $additional = []; foreach ($value['plugins'] as $moredata) { $additional[$moredata['raw_component']] = $moredata; } $newdata[$value['plugin_type_raw']] = $additional; } return $newdata; } /** * Test that we can fetch metadata about users for the whole system and that it matches the system count. */ public function test_get_registry_metadata_count() { $data = $this->get_meta_data(); $plugintypes = \core_component::get_plugin_types(); // Check that we have the correct number of plugin types. $plugincount = count($plugintypes) + 1; // Plus one for core. $this->assertEquals($plugincount, count($data)); // Check that each plugin count matches. foreach ($plugintypes as $plugintype => $notused) { $plugins = \core_component::get_plugin_list($plugintype); $this->assertEquals(count($plugins), count($data[$plugintype])); } // Let's check core subsystems. // The Privacy API adds an extra component in the form of 'core'. $coresubsystems = \core_component::get_core_subsystems(); $this->assertEquals(count($coresubsystems) + 1, count($data['core'])); } /** * Check that the expected null provider information is returned. */ public function test_get_registry_metadata_null_provider_details() { $data = $this->get_meta_data(); // Check details of core privacy (a null privder) are correct. $coreprivacy = $data['core']['core_privacy']; $this->assertEquals(1, $coreprivacy['compliant']); $this->assertNotEmpty($coreprivacy['nullprovider']); } /** * Check that the expected privacy provider information is returned. */ public function test_get_registry_metadata_provider_details() { $data = $this->get_meta_data(); // Check details of core rating (a normal provider) are correct. $corerating = $data['core']['core_rating']; $this->assertEquals(1, $corerating['compliant']); $this->assertNotEmpty($corerating['metadata']); $this->assertEquals('database_table', $corerating['metadata'][0]['type']); $this->assertNotEmpty($corerating['metadata'][0]['fields']); } } tests/task/task_test.php 0000644 00000022176 15152701722 0011374 0 ustar 00 <?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/>. namespace tool_dataprivacy\task; use tool_dataprivacy\api; defined('MOODLE_INTERNAL') || die(); require_once(__DIR__ . '/../data_privacy_testcase.php'); /** * Tests for scheduled tasks. * * @package tool_dataprivacy * @copyright 2018 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class task_test extends \data_privacy_testcase { /** * Test tearDown. */ public function tearDown(): void { \core_privacy\local\request\writer::reset(); } /** * Ensure that a delete data request for pre-existing deleted users * is created when there are not any existing data requests * for that particular user. */ public function test_delete_existing_deleted_users_task_no_previous_requests() { global $DB; $this->resetAfterTest(); $this->setAdminUser(); // Enable automatic creation of delete data requests. set_config('automaticdeletionrequests', 1, 'tool_dataprivacy'); // Create a user. $user = $this->getDataGenerator()->create_user(); // Mark the user as deleted. $user->deleted = 1; $DB->update_record('user', $user); // The user should not have a delete data request. $this->assertCount(0, api::get_data_requests($user->id, [], [api::DATAREQUEST_TYPE_DELETE])); $this->execute_task('tool_dataprivacy\task\delete_existing_deleted_users'); // After running the scheduled task, the deleted user should have a delete data request. $this->assertCount(1, api::get_data_requests($user->id, [], [api::DATAREQUEST_TYPE_DELETE])); } /** * Ensure that a delete data request for pre-existing deleted users * is not being created when automatic creation of delete data requests is disabled. */ public function test_delete_existing_deleted_users_task_automatic_creation_disabled() { global $DB; $this->resetAfterTest(); $this->setAdminUser(); // Disable automatic creation of delete data requests. set_config('automaticdeletionrequests', 0, 'tool_dataprivacy'); // Create a user. $user = $this->getDataGenerator()->create_user(); // Mark the user as deleted. $user->deleted = 1; $DB->update_record('user', $user); // The user should not have a delete data request. $this->assertCount(0, api::get_data_requests($user->id, [], [api::DATAREQUEST_TYPE_DELETE])); $this->execute_task('tool_dataprivacy\task\delete_existing_deleted_users'); // After running the scheduled task, the deleted user should still not have a delete data request. $this->assertCount(0, api::get_data_requests($user->id, [], [api::DATAREQUEST_TYPE_DELETE])); } /** * Ensure that a delete data request for pre-existing deleted users * is created when there are existing non-delete data requests * for that particular user. */ public function test_delete_existing_deleted_users_task_existing_export_data_requests() { global $DB; $this->resetAfterTest(); $this->setAdminUser(); // Enable automatic creation of delete data requests. set_config('automaticdeletionrequests', 1, 'tool_dataprivacy'); // Create a user. $user = $this->getDataGenerator()->create_user(); // Create export data request for the user. api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT); // Mark the user as deleted. $user->deleted = 1; $DB->update_record('user', $user); // The user should have a export data request. $this->assertCount(1, api::get_data_requests($user->id, [], [api::DATAREQUEST_TYPE_EXPORT])); // The user should not have a delete data request. $this->assertCount(0, api::get_data_requests($user->id, [], [api::DATAREQUEST_TYPE_DELETE])); $this->execute_task('tool_dataprivacy\task\delete_existing_deleted_users'); // After running the scheduled task, the deleted user should have a delete data request. $this->assertCount(1, api::get_data_requests($user->id, [], [api::DATAREQUEST_TYPE_DELETE])); } /** * Ensure that a delete data request for pre-existing deleted users * is not created when there are existing ongoing delete data requests * for that particular user. */ public function test_delete_existing_deleted_users_task_existing_ongoing_delete_data_requests() { global $DB; $this->resetAfterTest(); $this->setAdminUser(); // Enable automatic creation of delete data requests. set_config('automaticdeletionrequests', 1, 'tool_dataprivacy'); // Create a user. $user = $this->getDataGenerator()->create_user(); $this->setUser($user); // Create delete data request for the user. $datarequest = api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE); $requestid = $datarequest->get('id'); api::update_request_status($requestid, api::DATAREQUEST_STATUS_AWAITING_APPROVAL); // The user should have an ongoing delete data request. $this->assertCount(1, api::get_data_requests($user->id, [api::DATAREQUEST_STATUS_AWAITING_APPROVAL], [api::DATAREQUEST_TYPE_DELETE])); // Mark the user as deleted. $user->deleted = 1; $DB->update_record('user', $user); // The user should still have the existing ongoing delete data request. $this->assertCount(1, \tool_dataprivacy\api::get_data_requests($user->id, [api::DATAREQUEST_STATUS_AWAITING_APPROVAL], [api::DATAREQUEST_TYPE_DELETE])); $this->execute_task('tool_dataprivacy\task\delete_existing_deleted_users'); // After running the scheduled task, the user should have only one delete data request. $this->assertCount(1, api::get_data_requests($user->id, [], [api::DATAREQUEST_TYPE_DELETE])); } /** * Ensure that a delete data request for pre-existing deleted users * is not created when there are existing finished delete data requests * for that particular user. */ public function test_delete_existing_deleted_users_task_existing_finished_delete_data_requests() { global $DB; $this->resetAfterTest(); $this->setAdminUser(); // Enable automatic creation of delete data requests. set_config('automaticdeletionrequests', 1, 'tool_dataprivacy'); // Create a user. $user = $this->getDataGenerator()->create_user(); $this->setUser($user); // Create delete data request for the user. $datarequest = api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE); $requestid = $datarequest->get('id'); api::update_request_status($requestid, api::DATAREQUEST_STATUS_CANCELLED); // The user should have a delete data request. $this->assertCount(1, api::get_data_requests($user->id, [], [api::DATAREQUEST_TYPE_DELETE])); // The user should not have an ongoing data requests. $this->assertFalse(api::has_ongoing_request($user->id, api::DATAREQUEST_TYPE_DELETE)); // Mark the user as deleted. $user->deleted = 1; $DB->update_record('user', $user); // The user should still have the existing cancelled delete data request. $this->assertCount(1, \tool_dataprivacy\api::get_data_requests($user->id, [api::DATAREQUEST_STATUS_CANCELLED], [api::DATAREQUEST_TYPE_DELETE])); $this->execute_task('tool_dataprivacy\task\delete_existing_deleted_users'); // After running the scheduled task, the user should still have one delete data requests. $this->assertCount(1, api::get_data_requests($user->id, [], [api::DATAREQUEST_TYPE_DELETE])); // The user should only have the existing cancelled delete data request. $this->assertCount(1, \tool_dataprivacy\api::get_data_requests($user->id, [api::DATAREQUEST_STATUS_CANCELLED], [api::DATAREQUEST_TYPE_DELETE])); } /** * Helper to execute a particular task. * * @param string $task The task. */ private function execute_task($task) { // Run the scheduled task. ob_start(); $task = \core\task\manager::get_scheduled_task($task); $task->execute(); ob_end_clean(); } } tests/generator/lib.php 0000644 00000005724 15152701722 0011165 0 ustar 00 <?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/>. /** * Data privacy tool data generator. * * @package tool_dataprivacy * @category test * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use tool_dataprivacy\api; use tool_dataprivacy\category; use tool_dataprivacy\purpose; defined('MOODLE_INTERNAL') || die(); /** * Data privacy tool data generator class. * * @package tool_dataprivacy * @category test * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class tool_dataprivacy_generator extends component_generator_base { /** @var int Number of created categories. */ protected $categorycount = 0; /** @var int Number of created purposes. */ protected $purposecount = 0; /** * Reset process. * * Do not call directly. * * @return void */ public function reset() { $this->categorycount = 0; $this->purposecount = 0; } /** * Create a new category. * * @param array|stdClass $record * @return category */ public function create_category($record = null) { $this->categorycount++; $i = $this->categorycount; $record = (object)$record; if (!isset($record->name)) { $record->name = "Test purpose $i"; } if (!isset($record->description)) { $record->description = "{$record->name} description"; } $category = api::create_category($record); return $category; } /** * Create a new purpose. * * @param array|stdClass $record * @return purpose */ public function create_purpose($record = null) { $this->purposecount++; $i = $this->purposecount; $record = (object)$record; if (!isset($record->name)) { $record->name = "Test purpose $i"; } if (!isset($record->description)) { $record->description = "{$record->name} $i description"; } if (!isset($record->retentionperiod)) { $record->retentionperiod = 'PT1M'; } if (!isset($record->lawfulbases)) { $record->lawfulbases = 'gdpr_art_6_1_a'; } $purpose = api::create_purpose($record); return $purpose; } } tests/filtered_userlist_test.php 0000644 00000006732 15152701722 0013220 0 ustar 00 <?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/>. namespace tool_dataprivacy; /** * Unit tests for the filtered_userlist. * * @package tool_dataprivacy * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class filtered_userlist_test extends \advanced_testcase { /** * Test the apply_expired_contexts_filters function with arange of options. * * @dataProvider apply_expired_contexts_filters_provider * @param array $initial The set of userids in the initial filterlist. * @param array $expired The set of userids considered as expired. * @param array $unexpired The set of userids considered as unexpired. * @param array $expected The expected values. */ public function test_apply_expired_contexts_filters(array $initial, array $expired, array $unexpired, array $expected) { $userlist = $this->getMockBuilder(\tool_dataprivacy\filtered_userlist::class) ->disableOriginalConstructor() ->onlyMethods([]) ->getMock(); $rc = new \ReflectionClass(\tool_dataprivacy\filtered_userlist::class); $rcm = $rc->getMethod('set_userids'); $rcm->setAccessible(true); $rcm->invoke($userlist, $initial); $userlist->apply_expired_context_filters($expired, $unexpired); $filtered = $userlist->get_userids(); sort($expected); sort($filtered); $this->assertEquals($expected, $filtered); } /** * Data provider for the apply_expired_contexts_filters function. * * @return array */ public function apply_expired_contexts_filters_provider() : array { return [ // Entire list should be preserved. 'No overrides' => [ 'users' => [1, 2, 3, 4, 5], 'expired' => [], 'unexpired' => [], [1, 2, 3, 4, 5], ], // The list should be filtered to only keep the expired users. 'Expired only' => [ 'users' => [1, 2, 3, 4, 5], 'expired' => [2, 3, 4], 'unexpired' => [], 'expected' => [2, 3, 4], ], // The list should be filtered to remove any unexpired users. 'Unexpired only' => [ 'users' => [1, 2, 3, 4, 5], 'expired' => [], 'unexpired' => [1, 5], 'expected' => [2, 3, 4], ], // The list should be filtered to only keep expired users who are not on the unexpired list. 'Combination of expired and unexpired' => [ 'users' => [1, 2, 3, 4, 5], 'expired' => [1, 2, 3], 'unexpired' => [1, 5], 'expected' => [2, 3], ], ]; } } tests/data_registry_test.php 0000644 00000003626 15152701722 0012330 0 ustar 00 <?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/>. namespace tool_dataprivacy; /** * Unit tests for the data_registry class. * * @package tool_dataprivacy * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class data_registry_test extends \advanced_testcase { /** * Ensure that the get_effective_context_value only errors if provided an inappropriate element. * * This test is not great because we only test a limited set of values. This is a fault of the underlying API. */ public function test_get_effective_context_value_invalid_element() { $this->expectException(\coding_exception::class); data_registry::get_effective_context_value(\context_system::instance(), 'invalid'); } /** * Ensure that the get_effective_contextlevel_value only errors if provided an inappropriate element. * * This test is not great because we only test a limited set of values. This is a fault of the underlying API. */ public function test_get_effective_contextlevel_value_invalid_element() { $this->expectException(\coding_exception::class); data_registry::get_effective_contextlevel_value(\context_system::instance(), 'invalid'); } } tests/data_privacy_testcase.php 0000644 00000004231 15152701722 0012762 0 ustar 00 <?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/>. /** * Parent class for tests which need data privacy functionality. * * @package tool_dataprivacy * @copyright 2018 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Parent class for tests which need data privacy functionality. * * @package tool_dataprivacy * @copyright 2018 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class data_privacy_testcase extends advanced_testcase { /** * Assign one or more user IDs as site DPO * * @param stdClass|array $users User ID or array of user IDs to be assigned as site DPO * @return void */ protected function assign_site_dpo($users) { global $DB; $this->resetAfterTest(); if (!is_array($users)) { $users = array($users); } $context = context_system::instance(); // Give the manager role with the capability to manage data requests. $managerroleid = $DB->get_field('role', 'id', array('shortname' => 'manager')); assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $managerroleid, $context->id, true); // Assign user(s) as manager. foreach ($users as $user) { role_assign($managerroleid, $user->id, $context->id); } // Only map the manager role to the DPO role. set_config('dporoles', $managerroleid, 'tool_dataprivacy'); } } tests/behat/my_data_requests.feature 0000644 00000002207 15152701722 0013722 0 ustar 00 @tool @tool_dataprivacy Feature: Manage my own data requests In order to manage my own data requests As a user I need to be able to view and cancel all my data requests Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | s1@example.com | And the following config values are set as admin: | contactdataprotectionofficer | 1 | tool_dataprivacy | @javascript Scenario: Cancel my own data request Given I log in as "student1" And I follow "Profile" in the user menu And I click on "Contact the privacy officer" "link" And I set the field "Message" to "Hello DPO!" And I click on "Send" "button" in the "Contact the privacy officer" "dialogue" And I should see "Your request has been submitted to the privacy officer" When I click on "Data requests" "link" And I open the action menu in "Hello DPO!" "table_row" And I choose "Cancel" in the open action menu And I click on "Cancel request" "button" in the "Cancel request" "dialogue" Then I should see "Cancelled" in the "Hello DPO!" "table_row" tests/behat/manage_purposes.feature 0000644 00000007015 15152701722 0013543 0 ustar 00 @tool @tool_dataprivacy @javascript Feature: Manage data storage purposes As the privacy officer In order to manage the data registry I need to be able to manage the data storage purposes for the data registry Background: Given I log in as "admin" And I navigate to "Users > Privacy and policies > Data registry" in site administration And I open the action menu in "region-main" "region" And I choose "Purposes" in the open action menu And I press "Add purpose" And I set the following fields to these values: | Name | Purpose 1 | | Description | Purpose 1 description | | Lawful bases | Contract (GDPR Art. 6.1(b)),Legal obligation (GDPR Art 6.1(c)) | | Sensitive personal data processing reasons | Explicit consent (GDPR Art. 9.2(a)) | | retentionperiodnumber | 2 | When I press "Save" Then I should see "Purpose 1" in the "List of data purposes" "table" And I should see "Contract (GDPR Art. 6.1(b))" in the "Purpose 1" "table_row" And I should see "Legal obligation (GDPR Art 6.1(c))" in the "Purpose 1" "table_row" And I should see "Explicit consent (GDPR Art. 9.2(a))" in the "Purpose 1" "table_row" And I should see "2 years" in the "Purpose 1" "table_row" And "Purpose 1 Purpose 1 description" row "5" column of "List of data purposes" table should contain "No" Scenario: Update a data storage purpose Given I open the action menu in "Purpose 1" "table_row" And I choose "Edit" in the open action menu And I set the following fields to these values: | Name | Purpose 1 edited | | Description | Purpose 1 description edited | | Lawful bases | Contract (GDPR Art. 6.1(b)), Vital interests (GDPR Art. 6.1(d)) | | Sensitive personal data processing reasons | Explicit consent (GDPR Art. 9.2(a)) | | retentionperiodnumber | 3 | | protected | 1 | When I press "Save changes" Then I should see "Purpose 1 edited" in the "List of data purposes" "table" And I should see "Purpose 1 description edited" in the "Purpose 1 edited" "table_row" And I should see "Vital interests (GDPR Art. 6.1(d))" in the "Purpose 1 edited" "table_row" And I should see "3 years" in the "Purpose 1 edited" "table_row" But I should not see "Legal obligation (GDPR Art 6.1(c))" in the "Purpose 1 edited" "table_row" And "Purpose 1 edited Purpose 1 description edited" row "5" column of "List of data purposes" table should not contain "No" Scenario: Delete a data storage purpose Given I open the action menu in "Purpose 1" "table_row" And I choose "Delete" in the open action menu And I should see "Delete purpose" And I should see "Are you sure you want to delete the purpose 'Purpose 1'?" When I click on "Delete" "button" in the "Delete purpose" "dialogue" Then I should not see "Purpose 1" in the "List of data purposes" "table" tests/behat/behat_tool_dataprivacy.php 0000644 00000034103 15152701722 0014214 0 ustar 00 <?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/>. /** * Step definitions to generate database fixtures for the data privacy tool. * * @package tool_dataprivacy * @category test * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once(__DIR__ . '/../../../../../lib/behat/behat_base.php'); use Behat\Gherkin\Node\TableNode as TableNode; use Behat\Behat\Tester\Exception\PendingException as PendingException; use tool_dataprivacy\api; /** * Step definitions to generate database fixtures for the data privacy tool. * * @package tool_dataprivacy * @category test * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class behat_tool_dataprivacy extends behat_base { /** * Each element specifies: * - The data generator suffix used. * - The required fields. * - The mapping between other elements references and database field names. * @var array */ protected static $elements = array( 'categories' => array( 'datagenerator' => 'category', 'required' => array() ), 'purposes' => array( 'datagenerator' => 'purpose', 'required' => array() ), ); /** * Creates the specified element. More info about available elements in http://docs.moodle.org/dev/Acceptance_testing#Fixtures. * * @Given /^the following data privacy "(?P<element_string>(?:[^"]|\\")*)" exist:$/ * * @param string $elementname The name of the entity to add * @param TableNode $data */ public function the_following_data_categories_exist($elementname, TableNode $data) { // Now that we need them require the data generators. require_once(__DIR__.'/../../../../../lib/phpunit/classes/util.php'); if (empty(self::$elements[$elementname])) { throw new PendingException($elementname . ' data generator is not implemented'); } $datagenerator = testing_util::get_data_generator(); $dataprivacygenerator = $datagenerator->get_plugin_generator('tool_dataprivacy'); $elementdatagenerator = self::$elements[$elementname]['datagenerator']; $requiredfields = self::$elements[$elementname]['required']; if (!empty(self::$elements[$elementname]['switchids'])) { $switchids = self::$elements[$elementname]['switchids']; } foreach ($data->getHash() as $elementdata) { // Check if all the required fields are there. foreach ($requiredfields as $requiredfield) { if (!isset($elementdata[$requiredfield])) { throw new Exception($elementname . ' requires the field ' . $requiredfield . ' to be specified'); } } // Switch from human-friendly references to ids. if (isset($switchids)) { foreach ($switchids as $element => $field) { $methodname = 'get_' . $element . '_id'; // Not all the switch fields are required, default vars will be assigned by data generators. if (isset($elementdata[$element])) { // Temp $id var to avoid problems when $element == $field. $id = $this->{$methodname}($elementdata[$element]); unset($elementdata[$element]); $elementdata[$field] = $id; } } } // Preprocess the entities that requires a special treatment. if (method_exists($this, 'preprocess_' . $elementdatagenerator)) { $elementdata = $this->{'preprocess_' . $elementdatagenerator}($elementdata); } // Creates element. $methodname = 'create_' . $elementdatagenerator; if (method_exists($dataprivacygenerator, $methodname)) { // Using data generators directly. $dataprivacygenerator->{$methodname}($elementdata); } else if (method_exists($this, 'process_' . $elementdatagenerator)) { // Using an alternative to the direct data generator call. $this->{'process_' . $elementdatagenerator}($elementdata); } else { throw new PendingException($elementname . ' data generator is not implemented'); } } } /** * Sets the data category and data storage purpose for the site. * * @Given /^I set the site category and purpose to "(?P<category_string>(?:[^"]|\\")*)" and "(?P<purpose_string>(?:[^"]|\\")*)"$/ * * @param string $category The ID of the category to be set for the instance. * @param string $purpose The ID of the purpose to be set for the instance. */ public function i_set_the_site_category_and_purpose($category, $purpose) { $category = \tool_dataprivacy\category::get_record(['name' => $category]); $purpose = \tool_dataprivacy\purpose::get_record(['name' => $purpose]); $data = (object)[ 'contextlevel' => CONTEXT_SYSTEM, 'categoryid' => $category->get('id'), 'purposeid' => $purpose->get('id'), ]; api::set_contextlevel($data); } /** * Sets the data category and data storage purpose for a course category instance. * * @Given /^I set the category and purpose for the course category "(?P<categoryname_string>(?:[^"]|\\")*)" to "(?P<category_string>(?:[^"]|\\")*)" and "(?P<purpose_string>(?:[^"]|\\")*)"$/ * * @param string $name The instance name. It should match the name or the idnumber. * @param string $category The ID of the category to be set for the instance. * @param string $purpose The ID of the purpose to be set for the instance. */ public function i_set_the_category_and_purpose_for_course_category($name, $category, $purpose) { global $DB; $params = [ 'name' => $name, 'idnumber' => $name, ]; $select = 'name = :name OR idnumber = :idnumber'; $coursecatid = $DB->get_field_select('course_categories', 'id', $select, $params, MUST_EXIST); $context = context_coursecat::instance($coursecatid); $this->set_category_and_purpose($context->id, $category, $purpose); } /** * Sets the data category and data storage purpose for a course instance. * * @Given /^I set the category and purpose for the course "(?P<coursename_string>(?:[^"]|\\")*)" to "(?P<category_string>(?:[^"]|\\")*)" and "(?P<purpose_string>(?:[^"]|\\")*)"$/ * * @param string $name The instance name. It should match the fullname or the shortname, or the idnumber. * @param string $category The ID of the category to be set for the instance. * @param string $purpose The ID of the purpose to be set for the instance. */ public function i_set_the_category_and_purpose_for_course($name, $category, $purpose) { global $DB; $params = [ 'shortname' => $name, 'fullname' => $name, 'idnumber' => $name, ]; $select = 'shortname = :shortname OR fullname = :fullname OR idnumber = :idnumber'; $courseid = $DB->get_field_select('course', 'id', $select, $params, MUST_EXIST); $context = context_course::instance($courseid); $this->set_category_and_purpose($context->id, $category, $purpose); } /** * Sets the data category and data storage purpose for a course instance. * * @Given /^I set the category and purpose for the "(?P<activityname_string>(?:[^"]|\\")*)" "(?P<activitytype_string>(?:[^"]|\\")*)" in course "(?P<coursename_string>(?:[^"]|\\")*)" to "(?P<category_string>(?:[^"]|\\")*)" and "(?P<purpose_string>(?:[^"]|\\")*)"$/ * * @param string $name The instance name. It should match the name of the activity. * @param string $type The activity type. E.g. assign, quiz, forum, etc. * @param string $coursename The course name. It should match the fullname or the shortname, or the idnumber. * @param string $category The ID of the category to be set for the instance. * @param string $purpose The ID of the purpose to be set for the instance. */ public function i_set_the_category_and_purpose_for_activity($name, $type, $coursename, $category, $purpose) { global $DB; $params = [ 'shortname' => $coursename, 'fullname' => $coursename, 'idnumber' => $coursename, ]; $select = 'shortname = :shortname OR fullname = :fullname OR idnumber = :idnumber'; $courseid = $DB->get_field_select('course', 'id', $select, $params, MUST_EXIST); $cmid = null; $cms = get_coursemodules_in_course($type, $courseid); foreach ($cms as $cm) { if ($cm->name === $name || $cm->idnumber === $name) { $cmid = $cm->id; break; } } if ($cmid === null) { throw new coding_exception("Activity module '{$name}' of type '{$type}' not found!"); } $context = context_module::instance($cmid); $this->set_category_and_purpose($context->id, $category, $purpose); } /** * Sets the data category and data storage purpose for a course instance. * * @Given /^I set the category and purpose for the "(?P<blockname_string>(?:[^"]|\\")*)" block in the "(?P<coursename_string>(?:[^"]|\\")*)" course to "(?P<category_string>(?:[^"]|\\")*)" and "(?P<purpose_string>(?:[^"]|\\")*)"$/ * * @param string $name The instance name. It should match the name of the block. (e.g. online_users) * @param string $coursename The course name. It should match the fullname or the shortname, or the idnumber. * @param string $category The ID of the category to be set for the instance. * @param string $purpose The ID of the purpose to be set for the instance. */ public function i_set_the_category_and_purpose_for_block($name, $coursename, $category, $purpose) { global $DB; $params = [ 'shortname' => $coursename, 'fullname' => $coursename, 'idnumber' => $coursename, ]; $select = 'shortname = :shortname OR fullname = :fullname OR idnumber = :idnumber'; $courseid = $DB->get_field_select('course', 'id', $select, $params, MUST_EXIST); // Fetch the course context. $coursecontext = context_course::instance($courseid); // Fetch the block record and context. $blockid = $DB->get_field('block_instances', 'id', ['blockname' => $name, 'parentcontextid' => $coursecontext->id]); $context = context_block::instance($blockid); // Set the category and purpose. $this->set_category_and_purpose($context->id, $category, $purpose); } /** * Sets the category and purpose for a context instance. * * @param int $contextid The context ID. * @param int $categoryname The category name. * @param int $purposename The purpose name. * @throws coding_exception */ protected function set_category_and_purpose($contextid, $categoryname, $purposename) { $category = \tool_dataprivacy\category::get_record(['name' => $categoryname]); $purpose = \tool_dataprivacy\purpose::get_record(['name' => $purposename]); api::set_context_instance((object) [ 'contextid' => $contextid, 'purposeid' => $purpose->get('id'), 'categoryid' => $category->get('id'), ]); } /** * Create a dataprivacy request. * * @Given /^I create a dataprivacy "(?P<type_string>(?:[^"]|\\")*)" request for "(?P<user_string>(?:[^"]|\\")*)"$/ * * @param string $type The type of request to create (delete, or export) * @param string $username The username to create for */ public function i_create_a_dataprivacy_request_for($type, $username) { if ($type === 'delete') { $type = \tool_dataprivacy\api::DATAREQUEST_TYPE_DELETE; } else if ($type === 'export') { $type = \tool_dataprivacy\api::DATAREQUEST_TYPE_EXPORT; } else { throw new \Behat\Behat\Tester\Exception\ExpectationException("Unknown request type '{$type}'"); } $user = \core_user::get_user_by_username($username); \tool_dataprivacy\api::create_data_request($user->id, $type); } /** * Approve a dataprivacy request. * * @Given /^I approve a dataprivacy "(?P<type_string>(?:[^"]|\\")*)" request for "(?P<user_string>(?:[^"]|\\")*)"$/ * * @param string $type The type of request to create (delete, or export) * @param string $username The username to create for */ public function i_approve_a_dataprivacy_request_for($type, $username) { if ($type === 'delete') { $type = \tool_dataprivacy\api::DATAREQUEST_TYPE_DELETE; } else if ($type === 'export') { $type = \tool_dataprivacy\api::DATAREQUEST_TYPE_EXPORT; } else { throw new \Behat\Behat\Tester\Exception\ExpectationException("Unknown request type '{$type}'"); } $user = \core_user::get_user_by_username($username); $request = \tool_dataprivacy\data_request::get_record([ 'userid' => $user->id, 'type' => $type, 'status' => \tool_dataprivacy\api::DATAREQUEST_STATUS_AWAITING_APPROVAL, ]); \tool_dataprivacy\api::approve_data_request($request->get('id')); } } tests/behat/dataexport.feature 0000644 00000016531 15152701722 0012531 0 ustar 00 @tool @tool_dataprivacy Feature: Data export from the privacy API In order to export data for users and meet legal requirements As an admin, user, or parent I need to be able to export data for a user Background: Given the following "users" exist: | username | firstname | lastname | institution | | victim | Victim User | 1 | University1 | | victim2 | Victim User | 2 | University2 | | requester | The | Requester | University3 | | parent | Long-suffering | Parent | | And the following "roles" exist: | shortname | name | archetype | | tired | Tired | | And the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | tool/dataprivacy:makedatarequestsforchildren | Allow | tired | System | | | tool/dataprivacy:managedatarequests | Allow | manager | System | | | moodle/site:viewuseridentity | Prevent | manager | System | | And the following "role assigns" exist: | user | role | contextlevel | reference | | parent | tired | User | victim | And the following "system role assigns" exist: | user | role | contextlevel | | requester | manager | User | And the following config values are set as admin: | contactdataprotectionofficer | 1 | tool_dataprivacy | | privacyrequestexpiry | 55 | tool_dataprivacy | | dporoles | 1 | tool_dataprivacy | And the following data privacy "categories" exist: | name | | Site category | And the following data privacy "purposes" exist: | name | retentionperiod | | Site purpose | P10Y | And I set the site category and purpose to "Site category" and "Site purpose" @javascript Scenario: As admin, export data for a user and download it, unless it has expired Given I log in as "admin" And I navigate to "Users > Privacy and policies > Data requests" in site administration And I follow "New request" And I set the field "User" to "Victim User 1" And I press "Save changes" Then I should see "Victim User 1" And I should see "Awaiting approval" in the "Victim User 1" "table_row" And I open the action menu in "Victim User 1" "table_row" And I follow "Approve request" And I press "Approve request" And I should see "Approved" in the "Victim User 1" "table_row" And I run all adhoc tasks And I reload the page And I should see "Download ready" in the "Victim User 1" "table_row" And I open the action menu in "Victim User 1" "table_row" And following "Download" should download between "1" and "150000" bytes And the following config values are set as admin: | privacyrequestexpiry | 1 | tool_dataprivacy | And I wait "1" seconds And I navigate to "Users > Privacy and policies > Data requests" in site administration And I should see "Expired" in the "Victim User 1" "table_row" And I open the action menu in "Victim User 1" "table_row" And I should not see "Download" @javascript Scenario: As a student, request data export and then download it when approved, unless it has expired Given I log in as "victim" And I follow "Profile" in the user menu And I follow "Data requests" And I follow "New request" And I press "Save changes" Then I should see "Export all of my personal data" And I should see "Awaiting approval" in the "Export all of my personal data" "table_row" And I log out And I log in as "admin" And I navigate to "Users > Privacy and policies > Data requests" in site administration And I open the action menu in "Victim User 1" "table_row" And I follow "Approve request" And I press "Approve request" And I log out And I log in as "victim" And I follow "Profile" in the user menu And I follow "Data requests" And I should see "Approved" in the "Export all of my personal data" "table_row" And I run all adhoc tasks And I reload the page And I should see "Download ready" in the "Export all of my personal data" "table_row" And I open the action menu in "Victim User 1" "table_row" And following "Download" should download between "1" and "155000" bytes And the following config values are set as admin: | privacyrequestexpiry | 1 | tool_dataprivacy | And I wait "1" seconds And I reload the page And I should see "Expired" in the "Export all of my personal data" "table_row" And I should not see "Actions" @javascript Scenario: As a parent, request data export for my child because I don't trust the little blighter Given I log in as "parent" And I follow "Profile" in the user menu And I follow "Data requests" And I follow "New request" And I set the field "User" to "Victim User 1" And I press "Save changes" Then I should see "Victim User 1" And I should see "Awaiting approval" in the "Victim User 1" "table_row" And I log out And I log in as "admin" And I navigate to "Users > Privacy and policies > Data requests" in site administration And I open the action menu in "Victim User 1" "table_row" And I follow "Approve request" And I press "Approve request" And I log out And I log in as "parent" And I follow "Profile" in the user menu And I follow "Data requests" And I should see "Approved" in the "Victim User 1" "table_row" And I run all adhoc tasks And I reload the page And I should see "Download ready" in the "Victim User 1" "table_row" And I open the action menu in "Victim User 1" "table_row" And following "Download" should download between "1" and "150000" bytes And the following config values are set as admin: | privacyrequestexpiry | 1 | tool_dataprivacy | And I wait "1" seconds And I reload the page And I should see "Expired" in the "Victim User 1" "table_row" And I should not see "Actions" @javascript Scenario: Test search for user using extra field. Given the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | moodle/site:viewuseridentity | Allow | manager | System | | And the following config values are set as admin: | showuseridentity | institution | And I log in as "requester" And I navigate to "Users > Privacy and policies > Data requests" in site administration And I follow "New request" And I set the field "Search" to "University1" Then I should see "Victim User 1" When I reload the page And I set the field "Search" to "University2" Then I should see "Victim User 2" Scenario: Request data export as student with automatic approval turned on Given the following config values are set as admin: | automaticdataexportapproval | 1 | tool_dataprivacy | And I log in as "victim" And I follow "Profile" in the user menu And I follow "Export all of my personal data" When I press "Save changes" Then I should see "Your request has been submitted and will be processed soon." And I should see "Approved" in the "Export all of my personal data" "table_row" tests/behat/datadelete.feature 0000644 00000030000 15152701722 0012435 0 ustar 00 @tool @tool_dataprivacy Feature: Data delete from the privacy API In order to delete data for users and meet legal requirements As an admin, user, or parent I need to be able to request a user and their data data be deleted Background: Given the following "users" exist: | username | firstname | lastname | | victim | Victim User | 1 | | parent | Long-suffering | Parent | | privacyofficer | Privacy Officer | One | And the following "roles" exist: | shortname | name | archetype | | tired | Tired | | And the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | tool/dataprivacy:makedatarequestsforchildren | Allow | tired | System | | | tool/dataprivacy:makedatadeletionrequestsforchildren | Allow | tired | System | | | tool/dataprivacy:managedatarequests | Allow | manager | System | | And the following "role assigns" exist: | user | role | contextlevel | reference | | parent | tired | User | victim | And the following "system role assigns" exist: | user | role | contextlevel | | privacyofficer | manager | User | And the following config values are set as admin: | contactdataprotectionofficer | 1 | tool_dataprivacy | And the following data privacy "categories" exist: | name | | Site category | And the following data privacy "purposes" exist: | name | retentionperiod | | Site purpose | P10Y | And the following config values are set as admin: | contactdataprotectionofficer | 1 | tool_dataprivacy | | privacyrequestexpiry | 55 | tool_dataprivacy | | dporoles | 1 | tool_dataprivacy | And I set the site category and purpose to "Site category" and "Site purpose" @javascript Scenario: As admin, delete a user and their data Given I log in as "victim" And I should see "Victim User 1" And I log out And I log in as "admin" And I navigate to "Users > Privacy and policies > Data requests" in site administration And I follow "New request" And I set the field "User" to "Victim User 1" And I set the field "Type" to "Delete all of my personal data" And I press "Save changes" Then I should see "Victim User 1" And I should see "Awaiting approval" in the "Victim User 1" "table_row" And I open the action menu in "Victim User 1" "table_row" And I follow "Approve request" And I press "Approve request" And I should see "Approved" in the "Victim User 1" "table_row" And I run all adhoc tasks And I reload the page And I should see "Deleted" in the "Victim User 1" "table_row" And I log out And I log in as "victim" And I should see "Invalid login" @javascript Scenario: As a student, request deletion of account and data Given I log in as "victim" And I follow "Profile" in the user menu And I follow "Data requests" And I follow "New request" And I set the field "Type" to "Delete all of my personal data" And I press "Save changes" Then I should see "Delete all of my personal data" And I should see "Awaiting approval" in the "Delete all of my personal data" "table_row" And I log out And I log in as "admin" And I navigate to "Users > Privacy and policies > Data requests" in site administration And I open the action menu in "Victim User 1" "table_row" And I follow "Approve request" And I press "Approve request" And I log out And I log in as "victim" And I follow "Profile" in the user menu And I follow "Data requests" And I should see "Approved" in the "Delete all of my personal data" "table_row" And I run all adhoc tasks And I reload the page And I should see "Your session has timed out" And I log in as "victim" And I should see "Invalid login" And I log in as "admin" And I am on site homepage And I navigate to "Users > Privacy and policies > Data requests" in site administration And I should see "Deleted" @javascript Scenario: As a parent, request account and data deletion for my child Given I log in as "parent" And I follow "Profile" in the user menu And I follow "Data requests" And I follow "New request" And I set the field "User" to "Victim User 1" And I set the field "Type" to "Delete all of my personal data" And I press "Save changes" Then I should see "Victim User 1" And I should see "Awaiting approval" in the "Victim User 1" "table_row" And I log out And I log in as "admin" And I navigate to "Users > Privacy and policies > Data requests" in site administration And I open the action menu in "Victim User 1" "table_row" And I follow "Approve request" And I press "Approve request" And I log out And I log in as "parent" And I follow "Profile" in the user menu And I follow "Data requests" And I should see "Approved" in the "Victim User 1" "table_row" And I run all adhoc tasks And I reload the page And I should see "You don't have any personal data requests" @javascript Scenario: As a Privacy Officer, I cannot create data deletion request unless I have permission. Given I log in as "privacyofficer" And I navigate to "Users > Privacy and policies > Data requests" in site administration And I follow "New request" And I open the autocomplete suggestions list And I click on "Victim User 1" item in the autocomplete list Then I should see "Export all of my personal data" And "Type" "select" should not be visible And the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | tool/dataprivacy:requestdeleteforotheruser | Allow | manager | System | | And I reload the page And I open the autocomplete suggestions list And I click on "Victim User 1" item in the autocomplete list And "Type" "select" should be visible @javascript Scenario: As a student, I cannot create data deletion request unless I have permission. Given I log in as "victim" And I follow "Profile" in the user menu And I follow "Data requests" And I follow "New request" Then "Type" "select" should exist And the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | tool/dataprivacy:requestdelete | Prevent | user | System | | And I reload the page And I should see "Export all of my personal data" And "Type" "select" should not exist @javascript Scenario: As a parent, I cannot create data deletion request unless I have permission. Given I log in as "parent" And the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | tool/dataprivacy:makedatadeletionrequestsforchildren | Prevent | tired | System | victim | And I follow "Profile" in the user menu And I follow "Data requests" And I follow "New request" And I open the autocomplete suggestions list And I click on "Victim User 1" item in the autocomplete list And I set the field "Type" to "Delete all of my personal data" And I press "Save changes" And I should see "You don't have permission to create deletion request for this user." And the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | tool/dataprivacy:makedatadeletionrequestsforchildren | Allow | tired | System | victim | | tool/dataprivacy:requestdelete | Prevent | user | System | | And I open the autocomplete suggestions list And I click on "Long-suffering Parent" item in the autocomplete list And I press "Save changes" And I should see "You don't have permission to create deletion request for yourself." @javascript Scenario: As a student, link to create data deletion should not be shown if I don't have permission. Given the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | tool/dataprivacy:requestdelete | Prohibit | user | System | | When I log in as "victim" And I follow "Profile" in the user menu Then I should not see "Delete my account" @javascript Scenario: As a primary admin, the link to create a data deletion request should not be shown. Given I log in as "admin" When I follow "Profile" in the user menu Then I should not see "Delete my account" @javascript Scenario: As a Privacy Officer, I cannot Approve to Deny deletion data request without permission. Given the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | tool/dataprivacy:requestdeleteforotheruser | Allow | manager | System | | When I log in as "privacyofficer" And I navigate to "Users > Privacy and policies > Data requests" in site administration And I follow "New request" And I open the autocomplete suggestions list And I click on "Victim User 1" item in the autocomplete list And I set the field "Type" to "Delete all of my personal data" And I press "Save changes" And the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | tool/dataprivacy:requestdeleteforotheruser | Prohibit | manager | System | | And I reload the page Then ".selectrequests" "css_element" should not exist And I open the action menu in "region-main" "region" And I should not see "Approve request" And I should not see "Deny request" And I choose "View the request" in the open action menu And "Approve" "button" should not exist And "Deny" "button" should not exist @javascript Scenario: As a Privacy Officer, I cannot re-submit deletion data request without permission. Given the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | tool/dataprivacy:requestdeleteforotheruser | Allow | manager | System | | When I log in as "privacyofficer" And I navigate to "Users > Privacy and policies > Data requests" in site administration And I follow "New request" And I open the autocomplete suggestions list And I click on "Victim User 1" item in the autocomplete list And I set the field "Type" to "Delete all of my personal data" And I press "Save changes" And I open the action menu in "region-main" "region" And I follow "Deny request" And I press "Deny request" And the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | tool/dataprivacy:requestdeleteforotheruser | Prohibit | manager | System | | And I reload the page And I open the action menu in "region-main" "region" Then I should not see "Resubmit as new request" Scenario: Request data deletion as student with automatic approval turned on Given the following config values are set as admin: | automaticdatadeletionapproval | 1 | tool_dataprivacy | And I log in as "victim" And I follow "Profile" in the user menu And I follow "Delete my account" When I press "Save changes" Then I should see "Your request has been submitted and will be processed soon." And I should see "Approved" in the "Delete all of my personal data" "table_row" tests/behat/manage_categories.feature 0000644 00000003272 15152701722 0014011 0 ustar 00 @tool @tool_dataprivacy @javascript Feature: Manage data categories As the privacy officer In order to manage the data registry I need to be able to manage the data categories for the data registry Background: Given I log in as "admin" And I navigate to "Users > Privacy and policies > Data registry" in site administration And I open the action menu in "region-main" "region" And I choose "Categories" in the open action menu And I press "Add category" And I set the field "Name" to "Category 1" And I set the field "Description" to "Category 1 description" When I click on "Save" "button" in the "Add category" "dialogue" Then I should see "Category 1" in the "List of data categories" "table" And I should see "Category 1 description" in the "Category 1" "table_row" Scenario: Update a data category Given I open the action menu in "Category 1" "table_row" And I choose "Edit" in the open action menu And I set the field "Name" to "Category 1 edited" And I set the field "Description" to "Category 1 description edited" When I press "Save changes" Then I should see "Category 1 edited" in the "List of data categories" "table" And I should see "Category 1 description edited" in the "List of data categories" "table" Scenario: Delete a data category Given I open the action menu in "Category 1" "table_row" And I choose "Delete" in the open action menu And I should see "Delete category" And I should see "Are you sure you want to delete the category 'Category 1'?" When I click on "Delete" "button" in the "Delete category" "dialogue" Then I should not see "Category 1" in the "List of data categories" "table" tests/behat/manage_defaults.feature 0000644 00000044754 15152701722 0013505 0 ustar 00 @tool @tool_dataprivacy @javascript Feature: Manage data registry defaults As the privacy officer In order to manage the data registry I need to be able to manage the default data categories and data storage purposes for various context levels. Background: Given I log in as "admin" And the following "categories" exist: | name | idnumber | category | | Science and technology | scitech | | | Physics | st-phys | scitech | And the following "courses" exist: | fullname | shortname | category | | Fundamentals of physics 1 | Physics 101 | st-phys | And the following "activities" exist: | activity | name | idnumber | course | | assign | Assignment 1 | assign1 | Physics 101 | | forum | Forum 1 | forum1 | Physics 101 | And the following "blocks" exist: | blockname | contextlevel | reference | pagetypepattern | defaultregion | | online_users | Course | Physics 101 | course-view-* | site-post | And the following data privacy "categories" exist: | name | | Site category | | Category 1 | | Category 2 | And the following data privacy "purposes" exist: | name | retentionperiod | | Site purpose | P10Y | | Purpose 1 | P3Y | | Purpose 2 | P5Y | And I set the site category and purpose to "Site category" and "Site purpose" # Setting a default for course categories should apply to everything beneath that category. Scenario: Set course category data registry defaults Given I navigate to "Users > Privacy and policies > Data registry" in site administration And I click on "Set defaults" "link" And I should see "Inherit" And I press "Edit" And I set the field "Category" to "Category 1" And I set the field "Purpose" to "Purpose 1" When I press "Save changes" Then I should see "Category 1" And I should see "Purpose 1" And I navigate to "Users > Privacy and policies > Data registry" in site administration And I click on "Science and technology" "link" And I wait until the page is ready And the field "categoryid" matches value "Not set (use the default value)" And the field "purposeid" matches value "Not set (use the default value)" And I should see "3 years" And I click on "Courses" "link" in the ".data-registry" "css_element" And I wait until the page is ready And I click on "Physics 101" "link" And I wait until the page is ready And I should see "3 years" And I click on "Activities and resources" "link" And I wait until the page is ready And I should see "3 years" And I click on "Assignment 1 (Assignment)" "link" And I wait until the page is ready And I should see "3 years" # When Setting a default for course categories, and overriding a specific category, only that category and its # children will be overridden. # If any child is a course category, it will get the default. Scenario: Set course category data registry defaults with override Given I navigate to "Users > Privacy and policies > Data registry" in site administration And I click on "Set defaults" "link" And I press "Edit" And I set the field "Category" to "Category 1" And I set the field "Purpose" to "Purpose 1" And I press "Save changes" And I should see "Category 1" And I should see "Purpose 1" And I set the category and purpose for the course category "scitech" to "Category 2" and "Purpose 2" When I navigate to "Users > Privacy and policies > Data registry" in site administration And I click on "Science and technology" "link" And I wait until the page is ready Then the field "categoryid" matches value "Category 2" And the field "purposeid" matches value "Purpose 2" And I should see "5 years" And I click on "Courses" "link" in the ".data-registry" "css_element" And I wait until the page is ready # Physics 101 is also a category, so it will get the category default. And I click on "Physics 101" "link" And I wait until the page is ready And I should see "3 years" And I click on "Activities and resources" "link" And I wait until the page is ready And I should see "3 years" And I click on "Assignment 1 (Assignment)" "link" And I wait until the page is ready And I should see "3 years" # When overriding a specific category, only that category and its children will be overridden. Scenario: Set course category data registry defaults with override Given I set the category and purpose for the course category "scitech" to "Category 2" and "Purpose 2" When I navigate to "Users > Privacy and policies > Data registry" in site administration And I click on "Science and technology" "link" And I wait until the page is ready Then the field "categoryid" matches value "Category 2" And the field "purposeid" matches value "Purpose 2" And I should see "5 years" And I click on "Courses" "link" in the ".data-registry" "css_element" And I wait until the page is ready # Physics 101 is also a category, so it will get the category default. And I click on "Physics 101" "link" And I wait until the page is ready And I should see "5 years" And I click on "Activities and resources" "link" And I wait until the page is ready And I should see "5 years" And I click on "Assignment 1 (Assignment)" "link" And I wait until the page is ready And I should see "5 years" # Resetting instances removes custom values. Scenario: Set course category data registry defaults with override Given I set the category and purpose for the course category "scitech" to "Category 2" and "Purpose 2" And I navigate to "Users > Privacy and policies > Data registry" in site administration And I click on "Set defaults" "link" And I press "Edit" And I set the field "Category" to "Category 1" And I set the field "Purpose" to "Purpose 1" When I click on "Reset instances with custom values" "checkbox" And I press "Save changes" And I should see "Category 1" And I should see "Purpose 1" And I navigate to "Users > Privacy and policies > Data registry" in site administration And I click on "Science and technology" "link" And I wait until the page is ready Then the field "categoryid" matches value "Not set (use the default value)" And the field "purposeid" matches value "Not set (use the default value)" And I should see "3 years" Scenario: Set course data registry defaults Given I set the category and purpose for the course "Physics 101" to "Category 2" and "Purpose 2" And I navigate to "Users > Privacy and policies > Data registry" in site administration And I click on "Set defaults" "link" And I click on "Courses" "link" in the "#region-main" "css_element" And I should see "Inherit" And I should not see "Add a new module default" And I press "Edit" And I set the field "Category" to "Category 1" And I set the field "Purpose" to "Purpose 1" When I press "Save changes" Then I should see "Category 1" And I should see "Purpose 1" And I navigate to "Users > Privacy and policies > Data registry" in site administration And I click on "Science and technology" "link" And I wait until the page is ready And I click on "Courses" "link" in the ".data-registry" "css_element" And I wait until the page is ready And I click on "Physics 101" "link" And I wait until the page is ready And the field "categoryid" matches value "Category 2" And the field "purposeid" matches value "Purpose 2" And I should see "5 years (after the course end date)" And I click on "Activities and resources" "link" And I wait until the page is ready And I should see "5 years" And I click on "Assignment 1 (Assignment)" "link" And I wait until the page is ready And I should see "5 years" Scenario: Set course data registry defaults with override Given I set the category and purpose for the course "Physics 101" to "Category 2" and "Purpose 2" And I navigate to "Users > Privacy and policies > Data registry" in site administration And I click on "Set defaults" "link" And I click on "Courses" "link" in the "#region-main" "css_element" And I should see "Inherit" And I should not see "Add a new module default" And I press "Edit" And I set the field "Category" to "Category 1" And I set the field "Purpose" to "Purpose 1" And I click on "Reset instances with custom values" "checkbox" When I press "Save changes" Then I should see "Category 1" And I should see "Purpose 1" And I navigate to "Users > Privacy and policies > Data registry" in site administration And I click on "Science and technology" "link" And I wait until the page is ready And I click on "Courses" "link" in the ".data-registry" "css_element" And I wait until the page is ready And I click on "Physics 101" "link" And I wait until the page is ready And the field "categoryid" matches value "Not set (use the default value)" And the field "purposeid" matches value "Not set (use the default value)" And I should see "3 years (after the course end date)" And I click on "Activities and resources" "link" And I wait until the page is ready And I should see "3 years" And I click on "Assignment 1 (Assignment)" "link" And I wait until the page is ready And I should see "3 years" Scenario: Set module level data registry defaults Given I set the category and purpose for the "assign1" "assign" in course "Physics 101" to "Category 2" and "Purpose 2" And I navigate to "Users > Privacy and policies > Data registry" in site administration And I click on "Set defaults" "link" And I click on "Activity modules" "link" And I should see "Inherit" And I should see "Add a new module default" And I press "Edit" And I set the field "Category" to "Category 1" And I set the field "Purpose" to "Purpose 1" When I press "Save changes" Then I should see "Category 1" And I should see "Purpose 1" And I navigate to "Users > Privacy and policies > Data registry" in site administration And I click on "Science and technology" "link" And I wait until the page is ready And I click on "Courses" "link" in the ".data-registry" "css_element" And I wait until the page is ready And I click on "Physics 101" "link" And I wait until the page is ready And I click on "Activities and resources" "link" And I wait until the page is ready And I click on "Assignment 1 (Assignment)" "link" And I wait until the page is ready And the field "categoryid" matches value "Category 2" And the field "purposeid" matches value "Purpose 2" And I should see "5 years (after the course end date)" Scenario: Set module level data registry defaults with override Given I set the category and purpose for the "assign1" "assign" in course "Physics 101" to "Category 2" and "Purpose 2" And I navigate to "Users > Privacy and policies > Data registry" in site administration And I click on "Set defaults" "link" And I click on "Activity modules" "link" And I should see "Inherit" And I should see "Add a new module default" And I press "Edit" And I set the field "Category" to "Category 1" And I set the field "Purpose" to "Purpose 1" And I click on "Reset instances with custom values" "checkbox" When I press "Save changes" Then I should see "Category 1" And I should see "Purpose 1" And I navigate to "Users > Privacy and policies > Data registry" in site administration And I click on "Science and technology" "link" And I wait until the page is ready And I click on "Courses" "link" in the ".data-registry" "css_element" And I wait until the page is ready And I click on "Physics 101" "link" And I wait until the page is ready And I click on "Activities and resources" "link" And I wait until the page is ready And I click on "Assignment 1 (Assignment)" "link" And I wait until the page is ready And the field "categoryid" matches value "Not set (use the default value)" And the field "purposeid" matches value "Not set (use the default value)" And I click on "Forum 1 (Forum)" "link" And I wait until the page is ready And the field "categoryid" matches value "Not set (use the default value)" And the field "purposeid" matches value "Not set (use the default value)" And I should see "3 years (after the course end date)" Scenario: Set data registry defaults for an activity module Given I set the category and purpose for the "assign1" "assign" in course "Physics 101" to "Category 2" and "Purpose 2" And I navigate to "Users > Privacy and policies > Data registry" in site administration And I click on "Set defaults" "link" And I click on "Activity modules" "link" And I should see "Inherit" And I should see "Add a new module default" And I press "Add a new module default" And I set the field "Activity module" to "Assignment" And I set the field "Category" to "Category 1" And I set the field "Purpose" to "Purpose 1" When I press "Save changes" Then I should see "Category 1" in the "Assignment" "table_row" And I should see "Purpose 1" in the "Assignment" "table_row" And I navigate to "Users > Privacy and policies > Data registry" in site administration And I click on "Science and technology" "link" And I wait until the page is ready And I click on "Courses" "link" in the ".data-registry" "css_element" And I wait until the page is ready And I click on "Physics 101" "link" And I wait until the page is ready And I click on "Activities and resources" "link" And I wait until the page is ready And I click on "Assignment 1 (Assignment)" "link" And I wait until the page is ready And the field "categoryid" matches value "Category 2" And the field "purposeid" matches value "Purpose 2" And I should see "5 years (after the course end date)" Scenario: Set data registry defaults for an activity module with override Given I set the category and purpose for the "assign1" "assign" in course "Physics 101" to "Category 2" and "Purpose 2" And I navigate to "Users > Privacy and policies > Data registry" in site administration And I click on "Set defaults" "link" And I click on "Activity modules" "link" And I should see "Inherit" And I should see "Add a new module default" And I press "Add a new module default" And I set the field "Activity module" to "Assignment" And I set the field "Category" to "Category 1" And I set the field "Purpose" to "Purpose 1" And I click on "Reset instances with custom values" "checkbox" When I press "Save changes" Then I should see "Category 1" in the "Assignment" "table_row" And I should see "Purpose 1" in the "Assignment" "table_row" And I navigate to "Users > Privacy and policies > Data registry" in site administration And I click on "Science and technology" "link" And I wait until the page is ready And I click on "Courses" "link" in the ".data-registry" "css_element" And I wait until the page is ready And I click on "Physics 101" "link" And I wait until the page is ready And I click on "Activities and resources" "link" And I wait until the page is ready And I click on "Assignment 1 (Assignment)" "link" And I wait until the page is ready And the field "categoryid" matches value "Not set (use the default value)" And the field "purposeid" matches value "Not set (use the default value)" And I should see "3 years (after the course end date)" Scenario: Set block category data registry defaults Given I set the category and purpose for the "online_users" block in the "Physics 101" course to "Category 2" and "Purpose 2" And I navigate to "Users > Privacy and policies > Data registry" in site administration And I click on "Set defaults" "link" And I click on "Blocks" "link" And I should see "Inherit" And I should not see "Add a new module default" And I press "Edit" And I set the field "Category" to "Category 1" And I set the field "Purpose" to "Purpose 1" When I press "Save changes" Then I should see "Category 1" And I should see "Purpose 1" And I navigate to "Users > Privacy and policies > Data registry" in site administration And I click on "Science and technology" "link" And I wait until the page is ready And I click on "Courses" "link" in the ".data-registry" "css_element" And I wait until the page is ready And I click on "Physics 101" "link" And I wait until the page is ready And I click on "Blocks" "link" And I wait until the page is ready And I click on "Online users" "link" And I wait until the page is ready And the field "categoryid" matches value "Category 2" And the field "purposeid" matches value "Purpose 2" And I should see "5 years (after the course end date)" Scenario: Set course category data registry defaults with override Given I set the category and purpose for the "online_users" block in the "Physics 101" course to "Category 2" and "Purpose 2" And I navigate to "Users > Privacy and policies > Data registry" in site administration And I click on "Set defaults" "link" And I click on "Blocks" "link" And I should see "Inherit" And I should not see "Add a new module default" And I press "Edit" And I set the field "Category" to "Category 1" And I set the field "Purpose" to "Purpose 1" And I click on "Reset instances with custom values" "checkbox" When I press "Save changes" Then I should see "Category 1" And I should see "Purpose 1" And I navigate to "Users > Privacy and policies > Data registry" in site administration And I click on "Science and technology" "link" And I wait until the page is ready And I click on "Courses" "link" in the ".data-registry" "css_element" And I wait until the page is ready And I click on "Physics 101" "link" And I wait until the page is ready And I click on "Blocks" "link" And I wait until the page is ready And I click on "Online users" "link" And I wait until the page is ready And the field "categoryid" matches value "Not set (use the default value)" And the field "purposeid" matches value "Not set (use the default value)" And I should see "3 years (after the course end date)" tests/behat/contact_privacy_officer.feature 0000644 00000002260 15152701722 0015235 0 ustar 00 @tool @tool_dataprivacy Feature: Contact the privacy officer As a user In order to reach out to the site's privacy officer I need to be able to contact the site's privacy officer in Moodle Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | s1@example.com | @javascript Scenario: Contacting the privacy officer Given the following config values are set as admin: | contactdataprotectionofficer | 1 | tool_dataprivacy | When I log in as "student1" And I follow "Profile" in the user menu And I click on "Contact the privacy officer" "link" And I set the field "Message" to "Hello DPO!" And I click on "Send" "button" in the "Contact the privacy officer" "dialogue" Then I should see "Your request has been submitted to the privacy officer" And I click on "Data requests" "link" And I should see "Hello DPO!" in the "General enquiry" "table_row" Scenario: Contacting the privacy officer when not enabled When I log in as "student1" And I follow "Profile" in the user menu Then "Contact the privacy officer" "link" should not exist tests/behat/manage_data_requests.feature 0000644 00000015440 15152701722 0014530 0 ustar 00 @tool @tool_dataprivacy Feature: Manage data requests As the privacy officer In order to address the privacy-related requests I need to be able to manage the data requests of the site's users Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | John | Doe | s1@example.com | | student2 | Jane | Doe | s2@example.com | And the following config values are set as admin: | contactdataprotectionofficer | 1 | tool_dataprivacy | @javascript Scenario: Marking general enquiries as complete Given I log in as "student1" And I follow "Profile" in the user menu And I should see "Contact the privacy officer" And I click on "Contact the privacy officer" "link" And I set the field "Message" to "Hi PO! Can others access my information on your site?" And I click on "Send" "button" in the "Contact the privacy officer" "dialogue" And I should see "Your request has been submitted to the privacy officer" And I log out And I log in as "student2" And I follow "Profile" in the user menu And I click on "Contact the privacy officer" "link" And I set the field "Message" to "Dear Mr. Privacy Officer, I'd like to know more about GDPR. Thanks!" And I click on "Send" "button" in the "Contact the privacy officer" "dialogue" And I should see "Your request has been submitted to the privacy officer" And I log out When I log in as "admin" And I navigate to "Users > Privacy and policies > Data requests" in site administration Then I should see "Hi PO!" in the "John Doe" "table_row" And I should see "Dear Mr. Privacy Officer" in the "Jane Doe" "table_row" And I open the action menu in "John Doe" "table_row" And I should see "View the request" And I should see "Mark as complete" And I choose "View the request" in the open action menu And I should see "Hi PO! Can others access my information on your site?" And I press "Mark as complete" And I wait until the page is ready And I should see "Complete" in the "John Doe" "table_row" And I open the action menu in "John Doe" "table_row" And I should see "View the request" But I should not see "Mark as complete" And I press the escape key And I open the action menu in "Jane Doe" "table_row" And I choose "Mark as complete" in the open action menu And I should see "Do you really want to mark this user enquiry as complete?" And I press "Mark as complete" And I wait until the page is ready And I should see "Complete" in the "Jane Doe" "table_row" And I open the action menu in "Jane Doe" "table_row" And I should see "View the request" But I should not see "Mark as complete" @javascript Scenario: Bulk accepting requests Given I log in as "student1" And I follow "Profile" in the user menu And I should see "Data requests" And I click on "Data requests" "link" And I should see "New request" And I click on "New request" "link" And I should see "Type" And I should see "Comments" And I set the field "Type" to "Export all of my personal data" And I set the field "Comments" to "Comment1" And I press "Save changes" And I should see "Your request has been submitted to the privacy officer" And I log out And I log in as "student2" And I follow "Profile" in the user menu And I should see "Data requests" And I click on "Data requests" "link" And I should see "New request" And I click on "New request" "link" And I should see "Type" And I should see "Comments" And I set the field "Type" to "Export all of my personal data" And I set the field "Comments" to "Comment2" And I press "Save changes" And I should see "Your request has been submitted to the privacy officer" And I log out And I trigger cron And I log in as "admin" And I navigate to "Users > Privacy and policies > Data requests" in site administration And I should see "Comment1" in the "John Doe" "table_row" And I should see "Awaiting approval" in the "John Doe" "table_row" And I should see "Comment2" in the "Jane Doe" "table_row" And I should see "Awaiting approval" in the "Jane Doe" "table_row" And I click on ".selectrequests" "css_element" in the "John Doe" "table_row" And I click on ".selectrequests" "css_element" in the "Jane Doe" "table_row" And I set the field with xpath "//select[@id='bulk-action']" to "Approve" And I press "Confirm" And I should see "Approve requests" And I should see "Do you really want to bulk approve the selected data requests?" When I press "Approve requests" Then I should see "Approved" in the "John Doe" "table_row" And I should see "Approved" in the "Jane Doe" "table_row" @javascript Scenario: Bulk denying requests Given I log in as "student1" And I follow "Profile" in the user menu And I should see "Data requests" And I click on "Data requests" "link" And I should see "New request" And I click on "New request" "link" And I should see "Type" And I should see "Comments" And I set the field "Type" to "Export all of my personal data" And I set the field "Comments" to "Comment1" And I press "Save changes" And I should see "Your request has been submitted to the privacy officer" And I log out And I log in as "student2" And I follow "Profile" in the user menu And I should see "Data requests" And I click on "Data requests" "link" And I should see "New request" And I click on "New request" "link" And I should see "Type" And I should see "Comments" And I set the field "Type" to "Export all of my personal data" And I set the field "Comments" to "Comment2" And I press "Save changes" And I should see "Your request has been submitted to the privacy officer" And I log out And I trigger cron And I log in as "admin" And I navigate to "Users > Privacy and policies > Data requests" in site administration And I should see "Comment1" in the "John Doe" "table_row" And I should see "Awaiting approval" in the "John Doe" "table_row" And I should see "Comment2" in the "Jane Doe" "table_row" And I should see "Awaiting approval" in the "Jane Doe" "table_row" And I click on ".selectrequests" "css_element" in the "John Doe" "table_row" And I click on ".selectrequests" "css_element" in the "Jane Doe" "table_row" And I set the field with xpath "//select[@id='bulk-action']" to "Deny" And I press "Confirm" And I should see "Deny requests" And I should see "Do you really want to bulk deny the selected data requests?" When I press "Deny requests" Then I should see "Rejected" in the "John Doe" "table_row" And I should see "Rejected" in the "Jane Doe" "table_row" tests/behat/siteadmin_privacy_breadcrumbs.feature 0000644 00000003225 15152701722 0016435 0 ustar 00 @core @tool @tool_dataprivacy @javascript Feature: Verify the breadcrumbs in different privacy site administration pages Whenever I navigate to data registry page in site administration As an admin The breadcrumbs should be visible Background: Given I log in as "admin" Scenario: Verify the breadcrumbs in data registry page as an admin Given I navigate to "Users > Privacy and policies > Data registry" in site administration And "Data registry" "text" should exist in the ".breadcrumb" "css_element" And "Privacy and policies" "link" should exist in the ".breadcrumb" "css_element" When I click on "Set defaults" "link" Then "Set defaults" "text" should exist in the ".breadcrumb" "css_element" And "Data registry" "link" should exist in the ".breadcrumb" "css_element" And "Privacy and policies" "link" should exist in the ".breadcrumb" "css_element" And I navigate to "Users > Privacy and policies > Data registry" in site administration And I click on "Edit" "link" And I choose "Categories" in the open action menu And "Edit categories" "text" should exist in the ".breadcrumb" "css_element" And "Data registry" "link" should exist in the ".breadcrumb" "css_element" And "Privacy and policies" "link" should exist in the ".breadcrumb" "css_element" And I click on "Back" "link" And I click on "Edit" "link" And I choose "Purposes" in the open action menu And "Edit purposes" "text" should exist in the ".breadcrumb" "css_element" And "Data registry" "link" should exist in the ".breadcrumb" "css_element" And "Privacy and policies" "link" should exist in the ".breadcrumb" "css_element" tests/behat/protecteddelete.feature 0000644 00000007537 15152701722 0013540 0 ustar 00 @tool @tool_dataprivacy Feature: Protected data should not be deleted In order to delete data for users and meet legal requirements As an privacy office I need to be ensure that only expired or unprotected data is removed Background: Given the following "users" exist: | username | firstname | lastname | | u1 | u1 | u1 | And the following "courses" exist: | fullname | shortname | startdate | enddate | | C1 | C1 | ##1 year ago## | ##1 month ago## | | C2 | C2 | ##1 year ago## | ##last day of next month## | And the following "course enrolments" exist: | user | course | role | | u1 | C1 | student | | u1 | C2 | student | And the following "activities" exist: | activity | name | intro | course | idnumber | | forum | forump1 | Test forum description | C1 | forump1 | | forum | forumu1 | Test forum description | C1 | forumu1 | | forum | forump2 | Test forum description | C2 | forump2 | | forum | forumu2 | Test forum description | C2 | forumu2 | And the following data privacy "categories" exist: | name | | CAT | And the following data privacy "purposes" exist: | name | retentionperiod | protected | | Site purpose | PT1H | 0 | | prot | P1D | 1 | | unprot | P1D | 0 | And I set the category and purpose for the "forump1" "forum" in course "C1" to "CAT" and "prot" And I set the category and purpose for the "forump2" "forum" in course "C2" to "CAT" and "prot" And I set the category and purpose for the "forumu1" "forum" in course "C1" to "CAT" and "unprot" And I set the category and purpose for the "forumu2" "forum" in course "C2" to "CAT" and "unprot" And I set the site category and purpose to "CAT" and "Site purpose" @javascripta Scenario: Unexpired and protected data is not removed Given I log in as "u1" And I am on "C1" course homepage And I add a new discussion to "forump1" forum with: | Subject | Discussion subject | | Message | Test post in forump1 | And I am on "C1" course homepage And I add a new discussion to "forumu1" forum with: | Subject | Discussion subject | | Message | Test post in forumu1 | And I am on "C2" course homepage And I add a new discussion to "forump2" forum with: | Subject | Discussion subject | | Message | Test post in forump2 | And I am on "C2" course homepage And I add a new discussion to "forumu2" forum with: | Subject | Discussion subject | | Message | Test post in forumu2 | And I log out And I log in as "admin" And I create a dataprivacy "delete" request for "u1" And I approve a dataprivacy "delete" request for "u1" And I run all adhoc tasks And I navigate to "Users > Privacy and policies > Data requests" in site administration And I should see "Deleted" in the "u1" "table_row" And I am on "C1" course homepage And I follow "forump1" And I follow "Discussion subject" Then I should not see "Test post in forump1" When I am on "C1" course homepage And I follow "forumu1" And I follow "Discussion subject" Then I should not see "Test post in forumu1" And I am on "C2" course homepage And I follow "forump2" And I follow "Discussion subject" Then I should see "Test post in forump2" When I am on "C2" course homepage And I follow "forumu2" And I follow "Discussion subject" Then I should not see "Test post in forumu2" tests/expired_contexts_test.php 0000644 00000330046 15152701722 0013055 0 ustar 00 <?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/>. namespace tool_dataprivacy; /** * Expired contexts tests. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class expired_contexts_test extends \advanced_testcase { /** * Setup the basics with the specified retention period. * * @param string $system Retention policy for the system. * @param string $user Retention policy for users. * @param string $course Retention policy for courses. * @param string $activity Retention policy for activities. */ protected function setup_basics(string $system, string $user, string $course = null, string $activity = null) : \stdClass { $this->resetAfterTest(); $purposes = (object) [ 'system' => $this->create_and_set_purpose_for_contextlevel($system, CONTEXT_SYSTEM), 'user' => $this->create_and_set_purpose_for_contextlevel($user, CONTEXT_USER), ]; if (null !== $course) { $purposes->course = $this->create_and_set_purpose_for_contextlevel($course, CONTEXT_COURSE); } if (null !== $activity) { $purposes->activity = $this->create_and_set_purpose_for_contextlevel($activity, CONTEXT_MODULE); } return $purposes; } /** * Create a retention period and set it for the specified context level. * * @param string $retention * @param int $contextlevel * @return purpose */ protected function create_and_set_purpose_for_contextlevel(string $retention, int $contextlevel) : purpose { $purpose = new purpose(0, (object) [ 'name' => 'Test purpose ' . rand(1, 1000), 'retentionperiod' => $retention, 'lawfulbases' => 'gdpr_art_6_1_a', ]); $purpose->create(); $cat = new category(0, (object) ['name' => 'Test category']); $cat->create(); if ($contextlevel <= CONTEXT_USER) { $record = (object) [ 'purposeid' => $purpose->get('id'), 'categoryid' => $cat->get('id'), 'contextlevel' => $contextlevel, ]; api::set_contextlevel($record); } else { list($purposevar, ) = data_registry::var_names_from_context( \context_helper::get_class_for_level($contextlevel) ); set_config($purposevar, $purpose->get('id'), 'tool_dataprivacy'); } return $purpose; } /** * Ensure that a user with no lastaccess is not flagged for deletion. */ public function test_flag_not_setup() { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN); $context = \context_block::instance($block->instance->id); $this->setUser(); // Flag all expired contexts. $manager = new \tool_dataprivacy\expired_contexts_manager(); list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts(); $this->assertEquals(0, $flaggedcourses); $this->assertEquals(0, $flaggedusers); } /** * Ensure that a user with no lastaccess is not flagged for deletion. */ public function test_flag_user_no_lastaccess() { $this->resetAfterTest(); $this->setup_basics('PT1H', 'PT1H', 'PT1H'); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN); $context = \context_block::instance($block->instance->id); $this->setUser(); // Flag all expired contexts. $manager = new \tool_dataprivacy\expired_contexts_manager(); list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts(); $this->assertEquals(0, $flaggedcourses); $this->assertEquals(0, $flaggedusers); } /** * Ensure that a user with a recent lastaccess is not flagged for deletion. */ public function test_flag_user_recent_lastaccess() { $this->resetAfterTest(); $this->setup_basics('PT1H', 'PT1H', 'PT1H'); $user = $this->getDataGenerator()->create_user(['lastaccess' => time()]); $this->setUser($user); $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN); $context = \context_block::instance($block->instance->id); $this->setUser(); // Flag all expired contexts. $manager = new \tool_dataprivacy\expired_contexts_manager(); list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts(); $this->assertEquals(0, $flaggedcourses); $this->assertEquals(0, $flaggedusers); } /** * Ensure that a user with a lastaccess in the past is flagged for deletion. */ public function test_flag_user_past_lastaccess() { $this->resetAfterTest(); $this->setup_basics('PT1H', 'PT1H', 'PT1H'); $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]); $this->setUser($user); $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN); $context = \context_block::instance($block->instance->id); $this->setUser(); // Flag all expired contexts. $manager = new \tool_dataprivacy\expired_contexts_manager(); list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts(); // Although there is a block in the user context, everything in the user context is regarded as one. $this->assertEquals(0, $flaggedcourses); $this->assertEquals(1, $flaggedusers); } /** * Ensure that a user with a lastaccess in the past but active enrolments is not flagged for deletion. */ public function test_flag_user_past_lastaccess_still_enrolled() { $this->resetAfterTest(); $this->setup_basics('PT1H', 'PT1H', 'PT1H'); $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]); $course = $this->getDataGenerator()->create_course(['startdate' => time(), 'enddate' => time() + YEARSECS]); $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student'); $otheruser = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($otheruser->id, $course->id, 'student'); $this->setUser($user); $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN); $context = \context_block::instance($block->instance->id); $this->setUser(); // Flag all expired contexts. $manager = new \tool_dataprivacy\expired_contexts_manager(); list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts(); $this->assertEquals(0, $flaggedcourses); $this->assertEquals(0, $flaggedusers); } /** * Ensure that a user with a lastaccess in the past and no active enrolments is flagged for deletion. */ public function test_flag_user_update_existing() { $this->resetAfterTest(); $this->setup_basics('PT1H', 'PT1H', 'P5Y'); $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]); $usercontext = \context_user::instance($user->id); // Create an existing expired_context. $expiredcontext = new expired_context(0, (object) [ 'contextid' => $usercontext->id, 'defaultexpired' => 0, 'status' => expired_context::STATUS_EXPIRED, ]); $expiredcontext->save(); $this->assertEquals(0, $expiredcontext->get('defaultexpired')); // Flag all expired contexts. $manager = new \tool_dataprivacy\expired_contexts_manager(); list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts(); $this->assertEquals(0, $flaggedcourses); $this->assertEquals(1, $flaggedusers); // The user context will now have expired. $updatedcontext = new expired_context($expiredcontext->get('id')); $this->assertEquals(1, $updatedcontext->get('defaultexpired')); } /** * Ensure that a user with a lastaccess in the past and expired enrolments. */ public function test_flag_user_past_lastaccess_unexpired_past_enrolment() { $this->resetAfterTest(); $this->setup_basics('PT1H', 'PT1H', 'P1Y'); $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]); $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - WEEKSECS]); $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student'); $otheruser = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($otheruser->id, $course->id, 'student'); $this->setUser($user); $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN); $context = \context_block::instance($block->instance->id); $this->setUser(); // Flag all expired contexts. $manager = new \tool_dataprivacy\expired_contexts_manager(); list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts(); $this->assertEquals(0, $flaggedcourses); $this->assertEquals(0, $flaggedusers); } /** * Ensure that a user with a lastaccess in the past and expired enrolments. */ public function test_flag_user_past_override_role() { global $DB; $this->resetAfterTest(); $purposes = $this->setup_basics('PT1H', 'PT1H', 'PT1H'); $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]); $usercontext = \context_user::instance($user->id); $systemcontext = \context_system::instance(); $role = $DB->get_record('role', ['shortname' => 'manager']); $override = new purpose_override(0, (object) [ 'purposeid' => $purposes->user->get('id'), 'roleid' => $role->id, 'retentionperiod' => 'P5Y', ]); $override->save(); role_assign($role->id, $user->id, $systemcontext->id); // Flag all expired contexts. $manager = new \tool_dataprivacy\expired_contexts_manager(); list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts(); $this->assertEquals(0, $flaggedcourses); $this->assertEquals(0, $flaggedusers); $expiredrecord = expired_context::get_record(['contextid' => $usercontext->id]); $this->assertFalse($expiredrecord); } /** * Ensure that a user with a lastaccess in the past and expired enrolments. */ public function test_flag_user_past_lastaccess_expired_enrolled() { $this->resetAfterTest(); $this->setup_basics('PT1H', 'PT1H', 'PT1H'); $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]); $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - WEEKSECS]); $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student'); $otheruser = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($otheruser->id, $course->id, 'student'); $this->setUser($user); $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN); $context = \context_block::instance($block->instance->id); $this->setUser(); // Flag all expired contexts. $manager = new \tool_dataprivacy\expired_contexts_manager(); list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts(); $this->assertEquals(1, $flaggedcourses); $this->assertEquals(1, $flaggedusers); } /** * Ensure that a user with a lastaccess in the past and enrolments without a course end date are respected * correctly. */ public function test_flag_user_past_lastaccess_missing_enddate_required() { $this->resetAfterTest(); $this->setup_basics('PT1H', 'PT1H', 'PT1H'); $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]); $course = $this->getDataGenerator()->create_course(); $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student'); $otheruser = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($otheruser->id, $course->id, 'student'); $this->setUser($user); $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN); $context = \context_block::instance($block->instance->id); $this->setUser(); // Ensure that course end dates are not required. set_config('requireallenddatesforuserdeletion', 1, 'tool_dataprivacy'); // Flag all expired contexts. $manager = new \tool_dataprivacy\expired_contexts_manager(); list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts(); $this->assertEquals(0, $flaggedcourses); $this->assertEquals(0, $flaggedusers); } /** * Ensure that a user with a lastaccess in the past and enrolments without a course end date are respected * correctly when the end date is not required. */ public function test_flag_user_past_lastaccess_missing_enddate_not_required() { $this->resetAfterTest(); $this->setup_basics('PT1H', 'PT1H', 'PT1H'); $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]); $course = $this->getDataGenerator()->create_course(); $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student'); $otheruser = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($otheruser->id, $course->id, 'student'); $this->setUser($user); $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN); $context = \context_block::instance($block->instance->id); $this->setUser(); // Ensure that course end dates are required. set_config('requireallenddatesforuserdeletion', 0, 'tool_dataprivacy'); // Flag all expired contexts. $manager = new \tool_dataprivacy\expired_contexts_manager(); list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts(); $this->assertEquals(0, $flaggedcourses); $this->assertEquals(1, $flaggedusers); } /** * Ensure that a user with a recent lastaccess is not flagged for deletion. */ public function test_flag_user_recent_lastaccess_existing_record() { $this->resetAfterTest(); $this->setup_basics('PT1H', 'PT1H', 'PT1H'); $user = $this->getDataGenerator()->create_user(['lastaccess' => time()]); $usercontext = \context_user::instance($user->id); // Create an existing expired_context. $expiredcontext = new expired_context(0, (object) [ 'contextid' => $usercontext->id, 'status' => expired_context::STATUS_EXPIRED, ]); $expiredcontext->save(); $this->setUser($user); $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN); $context = \context_block::instance($block->instance->id); $this->setUser(); // Flag all expired contexts. $manager = new \tool_dataprivacy\expired_contexts_manager(); list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts(); $this->assertEquals(0, $flaggedcourses); $this->assertEquals(0, $flaggedusers); $this->expectException('dml_missing_record_exception'); new expired_context($expiredcontext->get('id')); } /** * Ensure that a user with a recent lastaccess is not flagged for deletion. */ public function test_flag_user_retention_changed() { $this->resetAfterTest(); $purposes = $this->setup_basics('PT1H', 'PT1H', 'PT1H'); $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - DAYSECS]); $usercontext = \context_user::instance($user->id); $this->setUser($user); $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN); $context = \context_block::instance($block->instance->id); $this->setUser(); // Flag all expired contexts. $manager = new \tool_dataprivacy\expired_contexts_manager(); list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts(); $this->assertEquals(0, $flaggedcourses); $this->assertEquals(1, $flaggedusers); $expiredcontext = expired_context::get_record(['contextid' => $usercontext->id]); $this->assertNotFalse($expiredcontext); // Increase the retention period to 5 years. $purposes->user->set('retentionperiod', 'P5Y'); $purposes->user->save(); // Re-run the expiry job - the previously flagged user will be removed because the retention period has been increased. list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts(); $this->assertEquals(0, $flaggedcourses); $this->assertEquals(0, $flaggedusers); // The expiry record will now have been removed. $this->expectException('dml_missing_record_exception'); new expired_context($expiredcontext->get('id')); } /** * Ensure that a user with a historically expired expired block record child is cleaned up. */ public function test_flag_user_historic_block_unapproved() { $this->resetAfterTest(); $this->setup_basics('PT1H', 'PT1H', 'PT1H'); $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - DAYSECS]); $usercontext = \context_user::instance($user->id); $this->setUser($user); $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN); $blockcontext = \context_block::instance($block->instance->id); $this->setUser(); // Create an existing expired_context which has not been approved for the block. $expiredcontext = new expired_context(0, (object) [ 'contextid' => $blockcontext->id, 'status' => expired_context::STATUS_EXPIRED, ]); $expiredcontext->save(); // Flag all expired contexts. $manager = new \tool_dataprivacy\expired_contexts_manager(); list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts(); $this->assertEquals(0, $flaggedcourses); $this->assertEquals(1, $flaggedusers); $expiredblockcontext = expired_context::get_record(['contextid' => $blockcontext->id]); $this->assertFalse($expiredblockcontext); $expiredusercontext = expired_context::get_record(['contextid' => $usercontext->id]); $this->assertNotFalse($expiredusercontext); } /** * Ensure that a user with a block which has a default retention period which has not expired, is still expired. */ public function test_flag_user_historic_unexpired_child() { $this->resetAfterTest(); $this->setup_basics('PT1H', 'PT1H', 'PT1H'); $this->create_and_set_purpose_for_contextlevel('P5Y', CONTEXT_BLOCK); $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - DAYSECS]); $usercontext = \context_user::instance($user->id); $this->setUser($user); $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN); $blockcontext = \context_block::instance($block->instance->id); $this->setUser(); // Flag all expired contexts. $manager = new \tool_dataprivacy\expired_contexts_manager(); list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts(); $this->assertEquals(0, $flaggedcourses); $this->assertEquals(1, $flaggedusers); $expiredcontext = expired_context::get_record(['contextid' => $usercontext->id]); $this->assertNotFalse($expiredcontext); } /** * Ensure that a course with no end date is not flagged. */ public function test_flag_course_no_enddate() { $this->resetAfterTest(); $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H'); $course = $this->getDataGenerator()->create_course(); $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); // Flag all expired contexts. $manager = new \tool_dataprivacy\expired_contexts_manager(); list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts(); $this->assertEquals(0, $flaggedcourses); $this->assertEquals(0, $flaggedusers); } /** * Ensure that a course with an end date in the distant past, but a child which is unexpired is not flagged. */ public function test_flag_course_past_enddate_future_child() { $this->resetAfterTest(); $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'P5Y'); $course = $this->getDataGenerator()->create_course([ 'startdate' => time() - (2 * YEARSECS), 'enddate' => time() - YEARSECS, ]); $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); // Flag all expired contexts. $manager = new \tool_dataprivacy\expired_contexts_manager(); list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts(); $this->assertEquals(0, $flaggedcourses); $this->assertEquals(0, $flaggedusers); } /** * Ensure that a course with an end date in the distant past is flagged. */ public function test_flag_course_past_enddate() { $this->resetAfterTest(); $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H'); $course = $this->getDataGenerator()->create_course([ 'startdate' => time() - (2 * YEARSECS), 'enddate' => time() - YEARSECS, ]); $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); // Flag all expired contexts. $manager = new \tool_dataprivacy\expired_contexts_manager(); list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts(); $this->assertEquals(2, $flaggedcourses); $this->assertEquals(0, $flaggedusers); } /** * Ensure that a course with an end date in the distant past is flagged. */ public function test_flag_course_past_enddate_multiple() { $this->resetAfterTest(); $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H'); $course1 = $this->getDataGenerator()->create_course([ 'startdate' => time() - (2 * YEARSECS), 'enddate' => time() - YEARSECS, ]); $forum1 = $this->getDataGenerator()->create_module('forum', ['course' => $course1->id]); $course2 = $this->getDataGenerator()->create_course([ 'startdate' => time() - (2 * YEARSECS), 'enddate' => time() - YEARSECS, ]); $forum2 = $this->getDataGenerator()->create_module('forum', ['course' => $course2->id]); // Flag all expired contexts. $manager = new \tool_dataprivacy\expired_contexts_manager(); list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts(); $this->assertEquals(4, $flaggedcourses); $this->assertEquals(0, $flaggedusers); } /** * Ensure that a course with an end date in the future is not flagged. */ public function test_flag_course_future_enddate() { $this->resetAfterTest(); $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H'); $course = $this->getDataGenerator()->create_course(['enddate' => time() + YEARSECS]); $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); // Flag all expired contexts. $manager = new \tool_dataprivacy\expired_contexts_manager(); list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts(); $this->assertEquals(0, $flaggedcourses); $this->assertEquals(0, $flaggedusers); } /** * Ensure that a course with an end date in the future is not flagged. */ public function test_flag_course_recent_unexpired_enddate() { $this->resetAfterTest(); $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H'); $course = $this->getDataGenerator()->create_course(['enddate' => time() - 1]); // Flag all expired contexts. $manager = new \tool_dataprivacy\expired_contexts_manager(); list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts(); $this->assertEquals(0, $flaggedcourses); $this->assertEquals(0, $flaggedusers); } /** * Ensure that a course with an end date in the distant past is flagged, taking into account any purpose override */ public function test_flag_course_past_enddate_with_override_unexpired_role() { global $DB; $this->resetAfterTest(); $purposes = $this->setup_basics('PT1H', 'PT1H', 'PT1H'); $role = $DB->get_record('role', ['shortname' => 'editingteacher']); $override = new purpose_override(0, (object) [ 'purposeid' => $purposes->course->get('id'), 'roleid' => $role->id, 'retentionperiod' => 'P5Y', ]); $override->save(); $course = $this->getDataGenerator()->create_course([ 'startdate' => time() - (2 * DAYSECS), 'enddate' => time() - DAYSECS, ]); $coursecontext = \context_course::instance($course->id); // Flag all expired contexts. $manager = new \tool_dataprivacy\expired_contexts_manager(); list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts(); $this->assertEquals(1, $flaggedcourses); $this->assertEquals(0, $flaggedusers); $expiredrecord = expired_context::get_record(['contextid' => $coursecontext->id]); $this->assertEmpty($expiredrecord->get('expiredroles')); $unexpiredroles = $expiredrecord->get('unexpiredroles'); $this->assertCount(1, $unexpiredroles); $this->assertContainsEquals($role->id, $unexpiredroles); } /** * Ensure that a course with an end date in the distant past is flagged, and any expired role is ignored. */ public function test_flag_course_past_enddate_with_override_expired_role() { global $DB; $this->resetAfterTest(); $purposes = $this->setup_basics('PT1H', 'PT1H', 'PT1H'); $role = $DB->get_record('role', ['shortname' => 'student']); // The role has a much shorter retention, but both should match. $override = new purpose_override(0, (object) [ 'purposeid' => $purposes->course->get('id'), 'roleid' => $role->id, 'retentionperiod' => 'PT1M', ]); $override->save(); $course = $this->getDataGenerator()->create_course([ 'startdate' => time() - (2 * DAYSECS), 'enddate' => time() - DAYSECS, ]); $coursecontext = \context_course::instance($course->id); // Flag all expired contexts. $manager = new \tool_dataprivacy\expired_contexts_manager(); list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts(); $this->assertEquals(1, $flaggedcourses); $this->assertEquals(0, $flaggedusers); $expiredrecord = expired_context::get_record(['contextid' => $coursecontext->id]); $this->assertEmpty($expiredrecord->get('expiredroles')); $this->assertEmpty($expiredrecord->get('unexpiredroles')); $this->assertTrue((bool) $expiredrecord->get('defaultexpired')); } /** * Ensure that where a course has explicitly expired one role, but that role is explicitly not expired in a child * context, does not have the parent context role expired. */ public function test_flag_course_override_expiredwith_override_unexpired_on_child() { global $DB; $this->resetAfterTest(); $purposes = $this->setup_basics('P1Y', 'P1Y', 'P1Y'); $role = $DB->get_record('role', ['shortname' => 'editingteacher']); (new purpose_override(0, (object) [ 'purposeid' => $purposes->course->get('id'), 'roleid' => $role->id, 'retentionperiod' => 'PT1S', ]))->save(); $modpurpose = new purpose(0, (object) [ 'name' => 'Module purpose', 'retentionperiod' => 'PT1S', 'lawfulbases' => 'gdpr_art_6_1_a', ]); $modpurpose->create(); (new purpose_override(0, (object) [ 'purposeid' => $modpurpose->get('id'), 'roleid' => $role->id, 'retentionperiod' => 'P5Y', ]))->save(); $course = $this->getDataGenerator()->create_course([ 'startdate' => time() - (2 * DAYSECS), 'enddate' => time() - DAYSECS, ]); $coursecontext = \context_course::instance($course->id); $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $cm = get_coursemodule_from_instance('forum', $forum->id); $forumcontext = \context_module::instance($cm->id); api::set_context_instance((object) [ 'contextid' => $forumcontext->id, 'purposeid' => $modpurpose->get('id'), 'categoryid' => 0, ]); // Flag all expired contexts. $manager = new \tool_dataprivacy\expired_contexts_manager(); list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts(); $this->assertEquals(1, $flaggedcourses); $this->assertEquals(0, $flaggedusers); // The course will not be expired as the default expiry has not passed, and the explicit role override has been // removed due to the child non-expiry. $expiredrecord = expired_context::get_record(['contextid' => $coursecontext->id]); $this->assertFalse($expiredrecord); // The forum will have an expiry for all _but_ the overridden role. $expiredrecord = expired_context::get_record(['contextid' => $forumcontext->id]); $this->assertEmpty($expiredrecord->get('expiredroles')); // The teacher is not expired. $unexpiredroles = $expiredrecord->get('unexpiredroles'); $this->assertCount(1, $unexpiredroles); $this->assertContainsEquals($role->id, $unexpiredroles); $this->assertTrue((bool) $expiredrecord->get('defaultexpired')); } /** * Ensure that a user context previously flagged as approved is not removed if the user has any unexpired roles. */ public function test_process_user_context_with_override_unexpired_role() { global $DB; $this->resetAfterTest(); $purposes = $this->setup_basics('PT1H', 'PT1H', 'PT1H'); $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]); $usercontext = \context_user::instance($user->id); $systemcontext = \context_system::instance(); $role = $DB->get_record('role', ['shortname' => 'manager']); $override = new purpose_override(0, (object) [ 'purposeid' => $purposes->user->get('id'), 'roleid' => $role->id, 'retentionperiod' => 'P5Y', ]); $override->save(); role_assign($role->id, $user->id, $systemcontext->id); // Create an existing expired_context. $expiredcontext = new expired_context(0, (object) [ 'contextid' => $usercontext->id, 'defaultexpired' => 1, 'status' => expired_context::STATUS_APPROVED, ]); $expiredcontext->add_unexpiredroles([$role->id]); $expiredcontext->save(); $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class) ->onlyMethods([ 'delete_data_for_user', 'delete_data_for_users_in_context', 'delete_data_for_all_users_in_context', ]) ->getMock(); $mockprivacymanager->expects($this->never())->method('delete_data_for_user'); $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context'); $mockprivacymanager->expects($this->never())->method('delete_data_for_users_in_context'); $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class) ->onlyMethods(['get_privacy_manager']) ->getMock(); $manager->method('get_privacy_manager')->willReturn($mockprivacymanager); $manager->set_progress(new \null_progress_trace()); list($processedcourses, $processedusers) = $manager->process_approved_deletions(); $this->assertEquals(0, $processedcourses); $this->assertEquals(0, $processedusers); $this->expectException('dml_missing_record_exception'); $updatedcontext = new expired_context($expiredcontext->get('id')); } /** * Ensure that a module context previously flagged as approved is removed with appropriate unexpiredroles kept. */ public function test_process_course_context_with_override_unexpired_role() { global $DB; $this->resetAfterTest(); $purposes = $this->setup_basics('PT1H', 'PT1H', 'PT1H'); $role = $DB->get_record('role', ['shortname' => 'editingteacher']); $override = new purpose_override(0, (object) [ 'purposeid' => $purposes->course->get('id'), 'roleid' => $role->id, 'retentionperiod' => 'P5Y', ]); $override->save(); $course = $this->getDataGenerator()->create_course([ 'startdate' => time() - (2 * YEARSECS), 'enddate' => time() - YEARSECS, ]); $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $cm = get_coursemodule_from_instance('forum', $forum->id); $forumcontext = \context_module::instance($cm->id); $generator = $this->getDataGenerator()->get_plugin_generator('mod_forum'); $student = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($student->id, $course->id, 'student'); $generator->create_discussion((object) [ 'course' => $forum->course, 'forum' => $forum->id, 'userid' => $student->id, ]); $teacher = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($teacher->id, $course->id, 'editingteacher'); $generator->create_discussion((object) [ 'course' => $forum->course, 'forum' => $forum->id, 'userid' => $teacher->id, ]); // Create an existing expired_context. $expiredcontext = new expired_context(0, (object) [ 'contextid' => $forumcontext->id, 'defaultexpired' => 1, 'status' => expired_context::STATUS_APPROVED, ]); $expiredcontext->add_unexpiredroles([$role->id]); $expiredcontext->save(); $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class) ->onlyMethods([ 'delete_data_for_user', 'delete_data_for_users_in_context', 'delete_data_for_all_users_in_context', ]) ->getMock(); $mockprivacymanager->expects($this->never())->method('delete_data_for_user'); $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context'); $mockprivacymanager ->expects($this->once()) ->method('delete_data_for_users_in_context') ->with($this->callback(function($userlist) use ($student, $teacher) { $forumlist = $userlist->get_userlist_for_component('mod_forum'); $userids = $forumlist->get_userids(); $this->assertCount(1, $userids); $this->assertContainsEquals($student->id, $userids); $this->assertNotContainsEquals($teacher->id, $userids); return true; })); $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class) ->onlyMethods(['get_privacy_manager']) ->getMock(); $manager->method('get_privacy_manager')->willReturn($mockprivacymanager); $manager->set_progress(new \null_progress_trace()); list($processedcourses, $processedusers) = $manager->process_approved_deletions(); $this->assertEquals(1, $processedcourses); $this->assertEquals(0, $processedusers); $updatedcontext = new expired_context($expiredcontext->get('id')); $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status')); } /** * Ensure that a module context previously flagged as approved is removed with appropriate expiredroles kept. */ public function test_process_course_context_with_override_expired_role() { global $DB; $this->resetAfterTest(); $purposes = $this->setup_basics('PT1H', 'PT1H', 'P5Y'); $role = $DB->get_record('role', ['shortname' => 'student']); $override = new purpose_override(0, (object) [ 'purposeid' => $purposes->course->get('id'), 'roleid' => $role->id, 'retentionperiod' => 'PT1M', ]); $override->save(); $course = $this->getDataGenerator()->create_course([ 'startdate' => time() - (2 * YEARSECS), 'enddate' => time() - YEARSECS, ]); $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $cm = get_coursemodule_from_instance('forum', $forum->id); $forumcontext = \context_module::instance($cm->id); $generator = $this->getDataGenerator()->get_plugin_generator('mod_forum'); $student = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($student->id, $course->id, 'student'); $generator->create_discussion((object) [ 'course' => $forum->course, 'forum' => $forum->id, 'userid' => $student->id, ]); $teacher = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($teacher->id, $course->id, 'editingteacher'); $generator->create_discussion((object) [ 'course' => $forum->course, 'forum' => $forum->id, 'userid' => $teacher->id, ]); // Create an existing expired_context. $expiredcontext = new expired_context(0, (object) [ 'contextid' => $forumcontext->id, 'defaultexpired' => 0, 'status' => expired_context::STATUS_APPROVED, ]); $expiredcontext->add_expiredroles([$role->id]); $expiredcontext->save(); $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class) ->onlyMethods([ 'delete_data_for_user', 'delete_data_for_users_in_context', 'delete_data_for_all_users_in_context', ]) ->getMock(); $mockprivacymanager->expects($this->never())->method('delete_data_for_user'); $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context'); $mockprivacymanager ->expects($this->once()) ->method('delete_data_for_users_in_context') ->with($this->callback(function($userlist) use ($student, $teacher) { $forumlist = $userlist->get_userlist_for_component('mod_forum'); $userids = $forumlist->get_userids(); $this->assertCount(1, $userids); $this->assertContainsEquals($student->id, $userids); $this->assertNotContainsEquals($teacher->id, $userids); return true; })); $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class) ->onlyMethods(['get_privacy_manager']) ->getMock(); $manager->method('get_privacy_manager')->willReturn($mockprivacymanager); $manager->set_progress(new \null_progress_trace()); list($processedcourses, $processedusers) = $manager->process_approved_deletions(); $this->assertEquals(1, $processedcourses); $this->assertEquals(0, $processedusers); $updatedcontext = new expired_context($expiredcontext->get('id')); $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status')); } /** * Ensure that a module context previously flagged as approved is removed with appropriate expiredroles kept. */ public function test_process_course_context_with_user_in_both_lists() { global $DB; $this->resetAfterTest(); $purposes = $this->setup_basics('PT1H', 'PT1H', 'P5Y'); $role = $DB->get_record('role', ['shortname' => 'student']); $override = new purpose_override(0, (object) [ 'purposeid' => $purposes->course->get('id'), 'roleid' => $role->id, 'retentionperiod' => 'PT1M', ]); $override->save(); $course = $this->getDataGenerator()->create_course([ 'startdate' => time() - (2 * YEARSECS), 'enddate' => time() - YEARSECS, ]); $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $cm = get_coursemodule_from_instance('forum', $forum->id); $forumcontext = \context_module::instance($cm->id); $generator = $this->getDataGenerator()->get_plugin_generator('mod_forum'); $teacher = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($teacher->id, $course->id, 'editingteacher'); $this->getDataGenerator()->enrol_user($teacher->id, $course->id, 'student'); $generator->create_discussion((object) [ 'course' => $forum->course, 'forum' => $forum->id, 'userid' => $teacher->id, ]); $student = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($student->id, $course->id, 'student'); $generator->create_discussion((object) [ 'course' => $forum->course, 'forum' => $forum->id, 'userid' => $student->id, ]); // Create an existing expired_context. $expiredcontext = new expired_context(0, (object) [ 'contextid' => $forumcontext->id, 'defaultexpired' => 0, 'status' => expired_context::STATUS_APPROVED, ]); $expiredcontext->add_expiredroles([$role->id]); $expiredcontext->save(); $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class) ->onlyMethods([ 'delete_data_for_user', 'delete_data_for_users_in_context', 'delete_data_for_all_users_in_context', ]) ->getMock(); $mockprivacymanager->expects($this->never())->method('delete_data_for_user'); $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context'); $mockprivacymanager ->expects($this->once()) ->method('delete_data_for_users_in_context') ->with($this->callback(function($userlist) use ($student, $teacher) { $forumlist = $userlist->get_userlist_for_component('mod_forum'); $userids = $forumlist->get_userids(); $this->assertCount(1, $userids); $this->assertContainsEquals($student->id, $userids); $this->assertNotContainsEquals($teacher->id, $userids); return true; })); $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class) ->onlyMethods(['get_privacy_manager']) ->getMock(); $manager->method('get_privacy_manager')->willReturn($mockprivacymanager); $manager->set_progress(new \null_progress_trace()); list($processedcourses, $processedusers) = $manager->process_approved_deletions(); $this->assertEquals(1, $processedcourses); $this->assertEquals(0, $processedusers); $updatedcontext = new expired_context($expiredcontext->get('id')); $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status')); } /** * Ensure that a module context previously flagged as approved is removed with appropriate expiredroles kept. */ public function test_process_course_context_with_user_in_both_lists_expired() { global $DB; $this->resetAfterTest(); $purposes = $this->setup_basics('PT1H', 'PT1H', 'P5Y'); $studentrole = $DB->get_record('role', ['shortname' => 'student']); $override = new purpose_override(0, (object) [ 'purposeid' => $purposes->course->get('id'), 'roleid' => $studentrole->id, 'retentionperiod' => 'PT1M', ]); $override->save(); $teacherrole = $DB->get_record('role', ['shortname' => 'editingteacher']); $override = new purpose_override(0, (object) [ 'purposeid' => $purposes->course->get('id'), 'roleid' => $teacherrole->id, 'retentionperiod' => 'PT1M', ]); $override->save(); $course = $this->getDataGenerator()->create_course([ 'startdate' => time() - (2 * YEARSECS), 'enddate' => time() - YEARSECS, ]); $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $cm = get_coursemodule_from_instance('forum', $forum->id); $forumcontext = \context_module::instance($cm->id); $generator = $this->getDataGenerator()->get_plugin_generator('mod_forum'); $teacher = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($teacher->id, $course->id, 'editingteacher'); $this->getDataGenerator()->enrol_user($teacher->id, $course->id, 'student'); $generator->create_discussion((object) [ 'course' => $forum->course, 'forum' => $forum->id, 'userid' => $teacher->id, ]); $student = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($student->id, $course->id, 'student'); $generator->create_discussion((object) [ 'course' => $forum->course, 'forum' => $forum->id, 'userid' => $student->id, ]); // Create an existing expired_context. $expiredcontext = new expired_context(0, (object) [ 'contextid' => $forumcontext->id, 'defaultexpired' => 0, 'status' => expired_context::STATUS_APPROVED, ]); $expiredcontext->add_expiredroles([$studentrole->id, $teacherrole->id]); $expiredcontext->save(); $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class) ->onlyMethods([ 'delete_data_for_user', 'delete_data_for_users_in_context', 'delete_data_for_all_users_in_context', ]) ->getMock(); $mockprivacymanager->expects($this->never())->method('delete_data_for_user'); $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context'); $mockprivacymanager ->expects($this->once()) ->method('delete_data_for_users_in_context') ->with($this->callback(function($userlist) use ($student, $teacher) { $forumlist = $userlist->get_userlist_for_component('mod_forum'); $userids = $forumlist->get_userids(); $this->assertCount(2, $userids); $this->assertContainsEquals($student->id, $userids); $this->assertContainsEquals($teacher->id, $userids); return true; })); $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class) ->onlyMethods(['get_privacy_manager']) ->getMock(); $manager->method('get_privacy_manager')->willReturn($mockprivacymanager); $manager->set_progress(new \null_progress_trace()); list($processedcourses, $processedusers) = $manager->process_approved_deletions(); $this->assertEquals(1, $processedcourses); $this->assertEquals(0, $processedusers); $updatedcontext = new expired_context($expiredcontext->get('id')); $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status')); } /** * Ensure that a site not setup will not process anything. */ public function test_process_not_setup() { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]); $usercontext = \context_user::instance($user->id); // Create an existing expired_context. $expiredcontext = new expired_context(0, (object) [ 'contextid' => $usercontext->id, 'status' => expired_context::STATUS_EXPIRED, ]); $expiredcontext->save(); $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class) ->onlyMethods([ 'delete_data_for_user', 'delete_data_for_all_users_in_context', ]) ->getMock(); $mockprivacymanager->expects($this->never())->method('delete_data_for_user'); $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context'); $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class) ->onlyMethods(['get_privacy_manager']) ->getMock(); $manager->set_progress(new \null_progress_trace()); $manager->method('get_privacy_manager')->willReturn($mockprivacymanager); list($processedcourses, $processedusers) = $manager->process_approved_deletions(); $this->assertEquals(0, $processedcourses); $this->assertEquals(0, $processedusers); } /** * Ensure that a user with no lastaccess is not flagged for deletion. */ public function test_process_none_approved() { $this->resetAfterTest(); $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H'); $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]); $usercontext = \context_user::instance($user->id); // Create an existing expired_context. $expiredcontext = new expired_context(0, (object) [ 'contextid' => $usercontext->id, 'status' => expired_context::STATUS_EXPIRED, ]); $expiredcontext->save(); $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class) ->onlyMethods([ 'delete_data_for_user', 'delete_data_for_all_users_in_context', ]) ->getMock(); $mockprivacymanager->expects($this->never())->method('delete_data_for_user'); $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context'); $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class) ->onlyMethods(['get_privacy_manager']) ->getMock(); $manager->set_progress(new \null_progress_trace()); $manager->method('get_privacy_manager')->willReturn($mockprivacymanager); list($processedcourses, $processedusers) = $manager->process_approved_deletions(); $this->assertEquals(0, $processedcourses); $this->assertEquals(0, $processedusers); } /** * Ensure that a user with no lastaccess is not flagged for deletion. */ public function test_process_no_context() { $this->resetAfterTest(); $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H'); // Create an existing expired_context. $expiredcontext = new expired_context(0, (object) [ 'contextid' => -1, 'status' => expired_context::STATUS_APPROVED, ]); $expiredcontext->save(); $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class) ->onlyMethods([ 'delete_data_for_user', 'delete_data_for_all_users_in_context', ]) ->getMock(); $mockprivacymanager->expects($this->never())->method('delete_data_for_user'); $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context'); $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class) ->onlyMethods(['get_privacy_manager']) ->getMock(); $manager->set_progress(new \null_progress_trace()); $manager->method('get_privacy_manager')->willReturn($mockprivacymanager); list($processedcourses, $processedusers) = $manager->process_approved_deletions(); $this->assertEquals(0, $processedcourses); $this->assertEquals(0, $processedusers); $this->expectException('dml_missing_record_exception'); new expired_context($expiredcontext->get('id')); } /** * Ensure that a user context previously flagged as approved is removed. */ public function test_process_user_context() { $this->resetAfterTest(); $this->setup_basics('PT1H', 'PT1H', 'PT1H'); $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]); $usercontext = \context_user::instance($user->id); $this->setUser($user); $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN); $blockcontext = \context_block::instance($block->instance->id); $this->setUser(); // Create an existing expired_context. $expiredcontext = new expired_context(0, (object) [ 'contextid' => $usercontext->id, 'status' => expired_context::STATUS_APPROVED, ]); $expiredcontext->save(); $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class) ->onlyMethods([ 'delete_data_for_user', 'delete_data_for_all_users_in_context', ]) ->getMock(); $mockprivacymanager->expects($this->atLeastOnce())->method('delete_data_for_user'); $mockprivacymanager->expects($this->exactly(2)) ->method('delete_data_for_all_users_in_context') ->withConsecutive( [$blockcontext], [$usercontext] ); $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class) ->onlyMethods(['get_privacy_manager']) ->getMock(); $manager->set_progress(new \null_progress_trace()); $manager->method('get_privacy_manager')->willReturn($mockprivacymanager); list($processedcourses, $processedusers) = $manager->process_approved_deletions(); $this->assertEquals(0, $processedcourses); $this->assertEquals(1, $processedusers); $updatedcontext = new expired_context($expiredcontext->get('id')); $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status')); // Flag all expired contexts again. list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts(); $this->assertEquals(0, $flaggedcourses); $this->assertEquals(0, $flaggedusers); // Ensure that the deleted context record is still present. $updatedcontext = new expired_context($expiredcontext->get('id')); $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status')); } /** * Ensure that a course context previously flagged as approved is removed. */ public function test_process_course_context() { $this->resetAfterTest(); $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H'); $course = $this->getDataGenerator()->create_course([ 'startdate' => time() - (2 * YEARSECS), 'enddate' => time() - YEARSECS, ]); $coursecontext = \context_course::instance($course->id); // Create an existing expired_context. $expiredcontext = new expired_context(0, (object) [ 'contextid' => $coursecontext->id, 'status' => expired_context::STATUS_APPROVED, ]); $expiredcontext->save(); $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class) ->onlyMethods([ 'delete_data_for_user', 'delete_data_for_all_users_in_context', ]) ->getMock(); $mockprivacymanager->expects($this->never())->method('delete_data_for_user'); $mockprivacymanager->expects($this->once())->method('delete_data_for_all_users_in_context'); $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class) ->onlyMethods(['get_privacy_manager']) ->getMock(); $manager->set_progress(new \null_progress_trace()); $manager->method('get_privacy_manager')->willReturn($mockprivacymanager); list($processedcourses, $processedusers) = $manager->process_approved_deletions(); $this->assertEquals(1, $processedcourses); $this->assertEquals(0, $processedusers); $updatedcontext = new expired_context($expiredcontext->get('id')); $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status')); } /** * Ensure that a user context previously flagged as approved is not removed if the user then logs in. */ public function test_process_user_context_logged_in_after_approval() { $this->resetAfterTest(); $this->setup_basics('PT1H', 'PT1H', 'PT1H'); $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]); $usercontext = \context_user::instance($user->id); $this->setUser($user); $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN); $context = \context_block::instance($block->instance->id); $this->setUser(); // Create an existing expired_context. $expiredcontext = new expired_context(0, (object) [ 'contextid' => $usercontext->id, 'status' => expired_context::STATUS_APPROVED, ]); $expiredcontext->save(); // Now bump the user's last login time. $this->setUser($user); user_accesstime_log(); $this->setUser(); $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class) ->onlyMethods([ 'delete_data_for_user', 'delete_data_for_all_users_in_context', ]) ->getMock(); $mockprivacymanager->expects($this->never())->method('delete_data_for_user'); $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context'); $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class) ->onlyMethods(['get_privacy_manager']) ->getMock(); $manager->set_progress(new \null_progress_trace()); $manager->method('get_privacy_manager')->willReturn($mockprivacymanager); list($processedcourses, $processedusers) = $manager->process_approved_deletions(); $this->assertEquals(0, $processedcourses); $this->assertEquals(0, $processedusers); $this->expectException('dml_missing_record_exception'); new expired_context($expiredcontext->get('id')); } /** * Ensure that a user context previously flagged as approved is not removed if the purpose has changed. */ public function test_process_user_context_changed_after_approved() { $this->resetAfterTest(); $this->setup_basics('PT1H', 'PT1H', 'PT1H'); $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]); $usercontext = \context_user::instance($user->id); $this->setUser($user); $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN); $context = \context_block::instance($block->instance->id); $this->setUser(); // Create an existing expired_context. $expiredcontext = new expired_context(0, (object) [ 'contextid' => $usercontext->id, 'status' => expired_context::STATUS_APPROVED, ]); $expiredcontext->save(); // Now make the user a site admin. $admins = explode(',', get_config('moodle', 'siteadmins')); $admins[] = $user->id; set_config('siteadmins', implode(',', $admins)); $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class) ->onlyMethods([ 'delete_data_for_user', 'delete_data_for_all_users_in_context', ]) ->getMock(); $mockprivacymanager->expects($this->never())->method('delete_data_for_user'); $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context'); $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class) ->onlyMethods(['get_privacy_manager']) ->getMock(); $manager->set_progress(new \null_progress_trace()); $manager->method('get_privacy_manager')->willReturn($mockprivacymanager); list($processedcourses, $processedusers) = $manager->process_approved_deletions(); $this->assertEquals(0, $processedcourses); $this->assertEquals(0, $processedusers); $this->expectException('dml_missing_record_exception'); new expired_context($expiredcontext->get('id')); } /** * Ensure that a user with a historically expired expired block record child is cleaned up. */ public function test_process_user_historic_block_unapproved() { $this->resetAfterTest(); $this->setup_basics('PT1H', 'PT1H', 'PT1H'); $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - DAYSECS]); $usercontext = \context_user::instance($user->id); $this->setUser($user); $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN); $blockcontext = \context_block::instance($block->instance->id); $this->setUser(); // Create an expired_context for the user. $expiredusercontext = new expired_context(0, (object) [ 'contextid' => $usercontext->id, 'status' => expired_context::STATUS_APPROVED, ]); $expiredusercontext->save(); // Create an existing expired_context which has not been approved for the block. $expiredblockcontext = new expired_context(0, (object) [ 'contextid' => $blockcontext->id, 'status' => expired_context::STATUS_EXPIRED, ]); $expiredblockcontext->save(); $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class) ->onlyMethods([ 'delete_data_for_user', 'delete_data_for_all_users_in_context', ]) ->getMock(); $mockprivacymanager->expects($this->atLeastOnce())->method('delete_data_for_user'); $mockprivacymanager->expects($this->exactly(2)) ->method('delete_data_for_all_users_in_context') ->withConsecutive( [$blockcontext], [$usercontext] ); $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class) ->onlyMethods(['get_privacy_manager']) ->getMock(); $manager->set_progress(new \null_progress_trace()); $manager->method('get_privacy_manager')->willReturn($mockprivacymanager); list($processedcourses, $processedusers) = $manager->process_approved_deletions(); $this->assertEquals(0, $processedcourses); $this->assertEquals(1, $processedusers); $updatedcontext = new expired_context($expiredusercontext->get('id')); $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status')); } /** * Ensure that a user with a block which has a default retention period which has not expired, is still expired. */ public function test_process_user_historic_unexpired_child() { $this->resetAfterTest(); $this->setup_basics('PT1H', 'PT1H', 'PT1H'); $this->create_and_set_purpose_for_contextlevel('P5Y', CONTEXT_BLOCK); $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - DAYSECS]); $usercontext = \context_user::instance($user->id); $this->setUser($user); $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN); $blockcontext = \context_block::instance($block->instance->id); $this->setUser(); // Create an expired_context for the user. $expiredusercontext = new expired_context(0, (object) [ 'contextid' => $usercontext->id, 'status' => expired_context::STATUS_APPROVED, ]); $expiredusercontext->save(); $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class) ->onlyMethods([ 'delete_data_for_user', 'delete_data_for_all_users_in_context', ]) ->getMock(); $mockprivacymanager->expects($this->atLeastOnce())->method('delete_data_for_user'); $mockprivacymanager->expects($this->exactly(2)) ->method('delete_data_for_all_users_in_context') ->withConsecutive( [$blockcontext], [$usercontext] ); $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class) ->onlyMethods(['get_privacy_manager']) ->getMock(); $manager->set_progress(new \null_progress_trace()); $manager->method('get_privacy_manager')->willReturn($mockprivacymanager); list($processedcourses, $processedusers) = $manager->process_approved_deletions(); $this->assertEquals(0, $processedcourses); $this->assertEquals(1, $processedusers); $updatedcontext = new expired_context($expiredusercontext->get('id')); $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status')); } /** * Ensure that a course context previously flagged as approved for deletion which now has an unflagged child, is * updated. */ public function test_process_course_context_updated() { $this->resetAfterTest(); $purposes = $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H'); $course = $this->getDataGenerator()->create_course([ 'startdate' => time() - (2 * YEARSECS), 'enddate' => time() - YEARSECS, ]); $coursecontext = \context_course::instance($course->id); $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); // Create an existing expired_context. $expiredcontext = new expired_context(0, (object) [ 'contextid' => $coursecontext->id, 'status' => expired_context::STATUS_APPROVED, ]); $expiredcontext->save(); $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class) ->onlyMethods([ 'delete_data_for_user', 'delete_data_for_all_users_in_context', ]) ->getMock(); $mockprivacymanager->expects($this->never())->method('delete_data_for_user'); $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context'); $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class) ->onlyMethods(['get_privacy_manager']) ->getMock(); $manager->set_progress(new \null_progress_trace()); $manager->method('get_privacy_manager')->willReturn($mockprivacymanager); // Changing the retention period to a longer period will remove the expired_context record. $purposes->activity->set('retentionperiod', 'P5Y'); $purposes->activity->save(); list($processedcourses, $processedusers) = $manager->process_approved_deletions(); $this->assertEquals(0, $processedcourses); $this->assertEquals(0, $processedusers); $this->expectException('dml_missing_record_exception'); $updatedcontext = new expired_context($expiredcontext->get('id')); } /** * Ensure that a course context previously flagged as approved for deletion which now has an unflagged child, is * updated. */ public function test_process_course_context_outstanding_children() { $this->resetAfterTest(); $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H'); $course = $this->getDataGenerator()->create_course([ 'startdate' => time() - (2 * YEARSECS), 'enddate' => time() - YEARSECS, ]); $coursecontext = \context_course::instance($course->id); $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); // Create an existing expired_context. $expiredcontext = new expired_context(0, (object) [ 'contextid' => $coursecontext->id, 'status' => expired_context::STATUS_APPROVED, ]); $expiredcontext->save(); $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class) ->onlyMethods([ 'delete_data_for_user', 'delete_data_for_all_users_in_context', ]) ->getMock(); $mockprivacymanager->expects($this->never())->method('delete_data_for_user'); $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context'); $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class) ->onlyMethods(['get_privacy_manager']) ->getMock(); $manager->set_progress(new \null_progress_trace()); $manager->method('get_privacy_manager')->willReturn($mockprivacymanager); list($processedcourses, $processedusers) = $manager->process_approved_deletions(); $this->assertEquals(0, $processedcourses); $this->assertEquals(0, $processedusers); $updatedcontext = new expired_context($expiredcontext->get('id')); // No change - we just can't process it until the children have finished. $this->assertEquals(expired_context::STATUS_APPROVED, $updatedcontext->get('status')); } /** * Ensure that a course context previously flagged as approved for deletion which now has an unflagged child, is * updated. */ public function test_process_course_context_pending_children() { $this->resetAfterTest(); $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H'); $course = $this->getDataGenerator()->create_course([ 'startdate' => time() - (2 * YEARSECS), 'enddate' => time() - YEARSECS, ]); $coursecontext = \context_course::instance($course->id); $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $cm = get_coursemodule_from_instance('forum', $forum->id); $forumcontext = \context_module::instance($cm->id); // Create an existing expired_context for the course. $expiredcoursecontext = new expired_context(0, (object) [ 'contextid' => $coursecontext->id, 'status' => expired_context::STATUS_APPROVED, ]); $expiredcoursecontext->save(); // And for the forum. $expiredforumcontext = new expired_context(0, (object) [ 'contextid' => $forumcontext->id, 'status' => expired_context::STATUS_EXPIRED, ]); $expiredforumcontext->save(); $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class) ->onlyMethods([ 'delete_data_for_user', 'delete_data_for_all_users_in_context', ]) ->getMock(); $mockprivacymanager->expects($this->never())->method('delete_data_for_user'); $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context'); $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class) ->onlyMethods(['get_privacy_manager']) ->getMock(); $manager->set_progress(new \null_progress_trace()); $manager->method('get_privacy_manager')->willReturn($mockprivacymanager); list($processedcourses, $processedusers) = $manager->process_approved_deletions(); $this->assertEquals(0, $processedcourses); $this->assertEquals(0, $processedusers); $updatedcontext = new expired_context($expiredcoursecontext->get('id')); // No change - we just can't process it until the children have finished. $this->assertEquals(expired_context::STATUS_APPROVED, $updatedcontext->get('status')); } /** * Ensure that a course context previously flagged as approved for deletion which now has an unflagged child, is * updated. */ public function test_process_course_context_approved_children() { $this->resetAfterTest(); $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H'); $course = $this->getDataGenerator()->create_course([ 'startdate' => time() - (2 * YEARSECS), 'enddate' => time() - YEARSECS, ]); $coursecontext = \context_course::instance($course->id); $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $cm = get_coursemodule_from_instance('forum', $forum->id); $forumcontext = \context_module::instance($cm->id); // Create an existing expired_context for the course. $expiredcoursecontext = new expired_context(0, (object) [ 'contextid' => $coursecontext->id, 'status' => expired_context::STATUS_APPROVED, ]); $expiredcoursecontext->save(); // And for the forum. $expiredforumcontext = new expired_context(0, (object) [ 'contextid' => $forumcontext->id, 'status' => expired_context::STATUS_APPROVED, ]); $expiredforumcontext->save(); $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class) ->onlyMethods([ 'delete_data_for_user', 'delete_data_for_all_users_in_context', ]) ->getMock(); $mockprivacymanager->expects($this->never())->method('delete_data_for_user'); $mockprivacymanager->expects($this->exactly(2)) ->method('delete_data_for_all_users_in_context') ->withConsecutive( [$forumcontext], [$coursecontext] ); $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class) ->onlyMethods(['get_privacy_manager']) ->getMock(); $manager->set_progress(new \null_progress_trace()); $manager->method('get_privacy_manager')->willReturn($mockprivacymanager); // Initially only the forum will be processed. list($processedcourses, $processedusers) = $manager->process_approved_deletions(); $this->assertEquals(1, $processedcourses); $this->assertEquals(0, $processedusers); $updatedcontext = new expired_context($expiredforumcontext->get('id')); $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status')); // The course won't have been processed yet. $updatedcontext = new expired_context($expiredcoursecontext->get('id')); $this->assertEquals(expired_context::STATUS_APPROVED, $updatedcontext->get('status')); // A subsequent run will cause the course to processed as it is no longer dependent upon the child contexts. list($processedcourses, $processedusers) = $manager->process_approved_deletions(); $this->assertEquals(1, $processedcourses); $this->assertEquals(0, $processedusers); $updatedcontext = new expired_context($expiredcoursecontext->get('id')); $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status')); } /** * Test that the can_process_deletion function returns expected results. * * @dataProvider can_process_deletion_provider * @param int $status * @param bool $expected */ public function test_can_process_deletion($status, $expected) { $purpose = new expired_context(0, (object) [ 'status' => $status, 'contextid' => \context_system::instance()->id, ]); $this->assertEquals($expected, $purpose->can_process_deletion()); } /** * Data provider for the can_process_deletion tests. * * @return array */ public function can_process_deletion_provider() : array { return [ 'Pending' => [ expired_context::STATUS_EXPIRED, false, ], 'Approved' => [ expired_context::STATUS_APPROVED, true, ], 'Complete' => [ expired_context::STATUS_CLEANED, false, ], ]; } /** * Test that the is_complete function returns expected results. * * @dataProvider is_complete_provider * @param int $status * @param bool $expected */ public function test_is_complete($status, $expected) { $purpose = new expired_context(0, (object) [ 'status' => $status, 'contextid' => \context_system::instance()->id, ]); $this->assertEquals($expected, $purpose->is_complete()); } /** * Data provider for the is_complete tests. * * @return array */ public function is_complete_provider() : array { return [ 'Pending' => [ expired_context::STATUS_EXPIRED, false, ], 'Approved' => [ expired_context::STATUS_APPROVED, false, ], 'Complete' => [ expired_context::STATUS_CLEANED, true, ], ]; } /** * Test that the is_fully_expired function returns expected results. * * @dataProvider is_fully_expired_provider * @param array $record * @param bool $expected */ public function test_is_fully_expired($record, $expected) { $purpose = new expired_context(0, (object) $record); $this->assertEquals($expected, $purpose->is_fully_expired()); } /** * Data provider for the is_fully_expired tests. * * @return array */ public function is_fully_expired_provider() : array { return [ 'Fully expired' => [ [ 'status' => expired_context::STATUS_APPROVED, 'defaultexpired' => 1, ], true, ], 'Unexpired roles present' => [ [ 'status' => expired_context::STATUS_APPROVED, 'defaultexpired' => 1, 'unexpiredroles' => json_encode([1]), ], false, ], 'Only some expired roles present' => [ [ 'status' => expired_context::STATUS_APPROVED, 'defaultexpired' => 0, 'expiredroles' => json_encode([1]), ], false, ], ]; } /** * Ensure that any orphaned records are removed once the context has been removed. */ public function test_orphaned_records_are_cleared() { $this->resetAfterTest(); $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H'); $course = $this->getDataGenerator()->create_course([ 'startdate' => time() - (2 * YEARSECS), 'enddate' => time() - YEARSECS, ]); $context = \context_course::instance($course->id); // Flag all expired contexts. $manager = new \tool_dataprivacy\expired_contexts_manager(); $manager->set_progress(new \null_progress_trace()); list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts(); $this->assertEquals(1, $flaggedcourses); $this->assertEquals(0, $flaggedusers); // Ensure that the record currently exists. $expiredcontext = expired_context::get_record(['contextid' => $context->id]); $this->assertNotFalse($expiredcontext); // Approve it. $expiredcontext->set('status', expired_context::STATUS_APPROVED)->save(); // Process deletions. list($processedcourses, $processedusers) = $manager->process_approved_deletions(); $this->assertEquals(1, $processedcourses); $this->assertEquals(0, $processedusers); // Ensure that the record still exists. $expiredcontext = expired_context::get_record(['contextid' => $context->id]); $this->assertNotFalse($expiredcontext); // Remove the actual course. delete_course($course->id, false); // The record will still exist until we flag it again. $expiredcontext = expired_context::get_record(['contextid' => $context->id]); $this->assertNotFalse($expiredcontext); list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts(); $expiredcontext = expired_context::get_record(['contextid' => $context->id]); $this->assertFalse($expiredcontext); } /** * Ensure that the progres tracer works as expected out of the box. */ public function test_progress_tracer_default() { $manager = new \tool_dataprivacy\expired_contexts_manager(); $rc = new \ReflectionClass(\tool_dataprivacy\expired_contexts_manager::class); $rcm = $rc->getMethod('get_progress'); $rcm->setAccessible(true); $this->assertInstanceOf(\text_progress_trace::class, $rcm->invoke($manager)); } /** * Ensure that the progres tracer works as expected when given a specific traer. */ public function test_progress_tracer_set() { $manager = new \tool_dataprivacy\expired_contexts_manager(); $mytrace = new \null_progress_trace(); $manager->set_progress($mytrace); $rc = new \ReflectionClass(\tool_dataprivacy\expired_contexts_manager::class); $rcm = $rc->getMethod('get_progress'); $rcm->setAccessible(true); $this->assertSame($mytrace, $rcm->invoke($manager)); } /** * Creates an HTML block on a user. * * @param string $title * @param string $body * @param string $format * @return \block_instance */ protected function create_user_block($title, $body, $format) { global $USER; $configdata = (object) [ 'title' => $title, 'text' => [ 'itemid' => 19, 'text' => $body, 'format' => $format, ], ]; $this->create_block($this->construct_user_page($USER)); $block = $this->get_last_block_on_page($this->construct_user_page($USER)); $block = block_instance('html', $block->instance); $block->instance_config_save((object) $configdata); return $block; } /** * Creates an HTML block on a page. * * @param \page $page Page */ protected function create_block($page) { $page->blocks->add_block_at_end_of_default_region('html'); } /** * Constructs a Page object for the User Dashboard. * * @param \stdClass $user User to create Dashboard for. * @return \moodle_page */ protected function construct_user_page(\stdClass $user) { $page = new \moodle_page(); $page->set_context(\context_user::instance($user->id)); $page->set_pagelayout('mydashboard'); $page->set_pagetype('my-index'); $page->blocks->load_blocks(); return $page; } /** * Get the last block on the page. * * @param \page $page Page * @return \block_html Block instance object */ protected function get_last_block_on_page($page) { $blocks = $page->blocks->get_blocks_for_region($page->blocks->get_default_region()); $block = end($blocks); return $block; } /** * Test the is_context_expired functions when supplied with the system context. */ public function test_is_context_expired_system() { $this->resetAfterTest(); $this->setup_basics('PT1H', 'PT1H', 'P1D'); $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]); $this->assertFalse(expired_contexts_manager::is_context_expired(\context_system::instance())); $this->assertFalse( expired_contexts_manager::is_context_expired_or_unprotected_for_user(\context_system::instance(), $user)); } /** * Test the is_context_expired functions when supplied with a block in the user context. * * Children of a user context always follow the user expiry rather than any context level defaults (e.g. at the * block level. */ public function test_is_context_expired_user_block() { $this->resetAfterTest(); $purposes = $this->setup_basics('PT1H', 'PT1H', 'P1D'); $purposes->block = $this->create_and_set_purpose_for_contextlevel('P5Y', CONTEXT_BLOCK); $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]); $this->setUser($user); $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN); $blockcontext = \context_block::instance($block->instance->id); $this->setUser(); // Protected flags have no bearing on expiry of user subcontexts. $this->assertTrue(expired_contexts_manager::is_context_expired($blockcontext)); $purposes->block->set('protected', 1)->save(); $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($blockcontext, $user)); $purposes->block->set('protected', 0)->save(); $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($blockcontext, $user)); } /** * Test the is_context_expired functions when supplied with the front page course. */ public function test_is_context_expired_frontpage() { $this->resetAfterTest(); $purposes = $this->setup_basics('PT1H', 'PT1H', 'P1D'); $frontcourse = get_site(); $frontcoursecontext = \context_course::instance($frontcourse->id); $sitenews = $this->getDataGenerator()->create_module('forum', ['course' => $frontcourse->id]); $cm = get_coursemodule_from_instance('forum', $sitenews->id); $sitenewscontext = \context_module::instance($cm->id); $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]); $this->assertFalse(expired_contexts_manager::is_context_expired($frontcoursecontext)); $this->assertFalse(expired_contexts_manager::is_context_expired($sitenewscontext)); $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($frontcoursecontext, $user)); $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($sitenewscontext, $user)); // Protecting the course contextlevel does not impact the front page. $purposes->course->set('protected', 1)->save(); $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($frontcoursecontext, $user)); $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($sitenewscontext, $user)); // Protecting the system contextlevel affects the front page, too. $purposes->system->set('protected', 1)->save(); $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($frontcoursecontext, $user)); $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($sitenewscontext, $user)); } /** * Test the is_context_expired functions when supplied with an expired course. */ public function test_is_context_expired_course_expired() { $this->resetAfterTest(); $purposes = $this->setup_basics('PT1H', 'PT1H', 'P1D'); $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]); $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time()]); $coursecontext = \context_course::instance($course->id); $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student'); $this->assertFalse(expired_contexts_manager::is_context_expired($coursecontext)); $purposes->course->set('protected', 1)->save(); $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user)); $purposes->course->set('protected', 0)->save(); $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user)); } /** * Test the is_context_expired functions when supplied with an unexpired course. */ public function test_is_context_expired_course_unexpired() { $this->resetAfterTest(); $purposes = $this->setup_basics('PT1H', 'PT1H', 'P1D'); $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]); $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - WEEKSECS]); $coursecontext = \context_course::instance($course->id); $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student'); $this->assertTrue(expired_contexts_manager::is_context_expired($coursecontext)); $purposes->course->set('protected', 1)->save(); $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user)); $purposes->course->set('protected', 0)->save(); $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user)); } /** * Test the is_context_expired functions when supplied with an unexpired course and a child context in the course which is protected. * * When a child context has a specific purpose set, then that purpose should be respected with respect to the * course. * * If the course is still within the expiry period for the child context, then that child's protected flag should be * respected, even when the course may have expired. */ public function test_is_child_context_expired_course_unexpired_with_child() { $this->resetAfterTest(); $purposes = $this->setup_basics('PT1H', 'PT1H', 'P1D', 'P1D'); $purposes->course->set('protected', 0)->save(); $purposes->activity->set('protected', 1)->save(); $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]); $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() + WEEKSECS]); $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $coursecontext = \context_course::instance($course->id); $cm = get_coursemodule_from_instance('forum', $forum->id); $forumcontext = \context_module::instance($cm->id); $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student'); $this->assertFalse(expired_contexts_manager::is_context_expired($coursecontext)); $this->assertFalse(expired_contexts_manager::is_context_expired($forumcontext)); $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user)); $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($forumcontext, $user)); $purposes->activity->set('protected', 0)->save(); $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($forumcontext, $user)); } /** * Test the is_context_expired functions when supplied with an expired course which has role overrides. */ public function test_is_context_expired_course_expired_override() { global $DB; $this->resetAfterTest(); $purposes = $this->setup_basics('PT1H', 'PT1H', 'PT1H'); $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]); $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - WEEKSECS]); $coursecontext = \context_course::instance($course->id); $systemcontext = \context_system::instance(); $role = $DB->get_record('role', ['shortname' => 'manager']); $override = new purpose_override(0, (object) [ 'purposeid' => $purposes->course->get('id'), 'roleid' => $role->id, 'retentionperiod' => 'P5Y', ]); $override->save(); role_assign($role->id, $user->id, $systemcontext->id); $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student'); $this->assertFalse(expired_contexts_manager::is_context_expired($coursecontext)); $purposes->course->set('protected', 1)->save(); $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user)); $purposes->course->set('protected', 0)->save(); $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user)); } /** * Test the is_context_expired functions when supplied with an expired course which has role overrides. */ public function test_is_context_expired_course_expired_override_parent() { global $DB; $this->resetAfterTest(); $purposes = $this->setup_basics('PT1H', 'PT1H'); $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]); $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - WEEKSECS]); $coursecontext = \context_course::instance($course->id); $systemcontext = \context_system::instance(); $role = $DB->get_record('role', ['shortname' => 'manager']); $override = new purpose_override(0, (object) [ 'purposeid' => $purposes->system->get('id'), 'roleid' => $role->id, 'retentionperiod' => 'P5Y', ]); $override->save(); role_assign($role->id, $user->id, $systemcontext->id); $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student'); $this->assertFalse(expired_contexts_manager::is_context_expired($coursecontext)); // The user override applies to this user. THIs means that the default expiry has no effect. $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user)); $purposes->system->set('protected', 1)->save(); $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user)); $purposes->system->set('protected', 0)->save(); $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user)); $override->set('protected', 1)->save(); $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user)); $purposes->system->set('protected', 1)->save(); $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user)); $purposes->system->set('protected', 0)->save(); $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user)); } /** * Test the is_context_expired functions when supplied with an expired course which has role overrides but the user * does not hold the role. */ public function test_is_context_expired_course_expired_override_parent_no_role() { global $DB; $this->resetAfterTest(); $purposes = $this->setup_basics('PT1H', 'PT1H'); $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]); $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - WEEKSECS]); $coursecontext = \context_course::instance($course->id); $systemcontext = \context_system::instance(); $role = $DB->get_record('role', ['shortname' => 'manager']); $override = new purpose_override(0, (object) [ 'purposeid' => $purposes->system->get('id'), 'roleid' => $role->id, 'retentionperiod' => 'P5Y', ]); $override->save(); $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student'); // This context is not _fully _ expired. $this->assertFalse(expired_contexts_manager::is_context_expired($coursecontext)); } /** * Test the is_context_expired functions when supplied with an unexpired course which has role overrides. */ public function test_is_context_expired_course_expired_override_inverse() { global $DB; $this->resetAfterTest(); $purposes = $this->setup_basics('P1Y', 'P1Y'); $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]); $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - WEEKSECS]); $coursecontext = \context_course::instance($course->id); $systemcontext = \context_system::instance(); $role = $DB->get_record('role', ['shortname' => 'student']); $override = new purpose_override(0, (object) [ 'purposeid' => $purposes->system->get('id'), 'roleid' => $role->id, 'retentionperiod' => 'PT1S', ]); $override->save(); $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student'); // This context is not _fully _ expired. $this->assertFalse(expired_contexts_manager::is_context_expired($coursecontext)); } /** * Test the is_context_expired functions when supplied with an unexpired course which has role overrides. */ public function test_is_context_expired_course_expired_override_inverse_parent() { global $DB; $this->resetAfterTest(); $purposes = $this->setup_basics('P1Y', 'P1Y'); $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]); $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - WEEKSECS]); $coursecontext = \context_course::instance($course->id); $systemcontext = \context_system::instance(); $role = $DB->get_record('role', ['shortname' => 'manager']); $override = new purpose_override(0, (object) [ 'purposeid' => $purposes->system->get('id'), 'roleid' => $role->id, 'retentionperiod' => 'PT1S', ]); $override->save(); $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student'); role_assign($role->id, $user->id, $systemcontext->id); $studentrole = $DB->get_record('role', ['shortname' => 'student']); role_unassign($studentrole->id, $user->id, $coursecontext->id); // This context is not _fully _ expired. $this->assertFalse(expired_contexts_manager::is_context_expired($coursecontext)); } /** * Test the is_context_expired functions when supplied with an unexpired course which has role overrides. */ public function test_is_context_expired_course_expired_override_inverse_parent_not_assigned() { global $DB; $this->resetAfterTest(); $purposes = $this->setup_basics('P1Y', 'P1Y'); $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]); $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - WEEKSECS]); $coursecontext = \context_course::instance($course->id); $systemcontext = \context_system::instance(); $role = $DB->get_record('role', ['shortname' => 'manager']); $override = new purpose_override(0, (object) [ 'purposeid' => $purposes->system->get('id'), 'roleid' => $role->id, 'retentionperiod' => 'PT1S', ]); $override->save(); // Enrol the user in the course without any role. $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student'); $studentrole = $DB->get_record('role', ['shortname' => 'student']); role_unassign($studentrole->id, $user->id, $coursecontext->id); // This context is not _fully _ expired. $this->assertFalse(expired_contexts_manager::is_context_expired($coursecontext)); } /** * Ensure that context expired checks for a specific user taken into account roles. */ public function test_is_context_expired_or_unprotected_for_user_role_mixtures_protected() { global $DB; $this->resetAfterTest(); $purposes = $this->setup_basics('PT1S', 'PT1S', 'PT1S'); $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - DAYSECS]); $coursecontext = \context_course::instance($course->id); $systemcontext = \context_system::instance(); $roles = $DB->get_records_menu('role', [], 'id', 'shortname, id'); $override = new purpose_override(0, (object) [ 'purposeid' => $purposes->course->get('id'), 'roleid' => $roles['manager'], 'retentionperiod' => 'P1W', 'protected' => 1, ]); $override->save(); $s = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($s->id, $course->id, 'student'); $t = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($t->id, $course->id, 'teacher'); $sm = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($sm->id, $course->id, 'student'); role_assign($roles['manager'], $sm->id, $coursecontext->id); $m = $this->getDataGenerator()->create_user(); role_assign($roles['manager'], $m->id, $coursecontext->id); $tm = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($t->id, $course->id, 'teacher'); role_assign($roles['manager'], $tm->id, $coursecontext->id); // The context should only be expired for users who are not a manager. $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $s)); $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $t)); $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $sm)); $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $tm)); $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $m)); $override->set('protected', 0)->save(); $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $s)); $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $t)); $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $sm)); $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $tm)); $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $m)); } /** * Ensure that context expired checks for a specific user taken into account roles when retention is inversed. */ public function test_is_context_expired_or_unprotected_for_user_role_mixtures_protected_inverse() { global $DB; $this->resetAfterTest(); $purposes = $this->setup_basics('P5Y', 'P5Y', 'P5Y'); $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - DAYSECS]); $coursecontext = \context_course::instance($course->id); $systemcontext = \context_system::instance(); $roles = $DB->get_records_menu('role', [], 'id', 'shortname, id'); $override = new purpose_override(0, (object) [ 'purposeid' => $purposes->course->get('id'), 'roleid' => $roles['student'], 'retentionperiod' => 'PT1S', ]); $override->save(); $s = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($s->id, $course->id, 'student'); $t = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($t->id, $course->id, 'teacher'); $sm = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($sm->id, $course->id, 'student'); role_assign($roles['manager'], $sm->id, $coursecontext->id); $m = $this->getDataGenerator()->create_user(); role_assign($roles['manager'], $m->id, $coursecontext->id); $tm = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($t->id, $course->id, 'teacher'); role_assign($roles['manager'], $tm->id, $coursecontext->id); // The context should only be expired for users who are only a student. $purposes->course->set('protected', 1)->save(); $override->set('protected', 1)->save(); $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $s)); $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $t)); $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $sm)); $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $tm)); $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $m)); $purposes->course->set('protected', 0)->save(); $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $s)); $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $t)); $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $sm)); $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $tm)); $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $m)); } } tests/external/external_test.php 0000644 00000125354 15152701722 0013136 0 ustar 00 <?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/>. /** * External tests. * * @package tool_dataprivacy * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\external; use externallib_advanced_testcase; use tool_dataprivacy\api; use tool_dataprivacy\context_instance; use tool_dataprivacy\external; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); /** * External testcase. * * @package tool_dataprivacy * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class external_test extends externallib_advanced_testcase { /** * Test for external::approve_data_request() with the user not logged in. */ public function test_approve_data_request_not_logged_in() { $this->resetAfterTest(); $generator = new \testing_data_generator(); $requester = $generator->create_user(); $comment = 'sample comment'; // Test data request creation. $this->setUser($requester); $datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment); // Log out the user and set force login to true. $this->setUser(); $this->expectException(\require_login_exception::class); external::approve_data_request($datarequest->get('id')); } /** * Test for external::approve_data_request() with the user not having a DPO role. */ public function test_approve_data_request_not_dpo() { $this->resetAfterTest(); $generator = new \testing_data_generator(); $requester = $generator->create_user(); $comment = 'sample comment'; // Test data request creation. $this->setUser($requester); $datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment); // Login as the requester. $this->setUser($requester); $this->expectException(\required_capability_exception::class); external::approve_data_request($datarequest->get('id')); } /** * Test for external::approve_data_request() for request that's not ready for approval */ public function test_approve_data_request_not_waiting_for_approval() { $this->resetAfterTest(); $generator = new \testing_data_generator(); $requester = $generator->create_user(); $comment = 'sample comment'; // Test data request creation. $this->setUser($requester); $datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment); $datarequest->set('status', api::DATAREQUEST_STATUS_CANCELLED)->save(); // Admin as DPO. (The default when no one's assigned as a DPO in the site). $this->setAdminUser(); $this->expectException(\moodle_exception::class); external::approve_data_request($datarequest->get('id')); } /** * Test for external::approve_data_request() */ public function test_approve_data_request() { $this->resetAfterTest(); $generator = new \testing_data_generator(); $requester = $generator->create_user(); $comment = 'sample comment'; // Test data request creation. $this->setUser($requester); $datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment); // Admin as DPO. (The default when no one's assigned as a DPO in the site). $this->setAdminUser(); api::update_request_status($datarequest->get('id'), api::DATAREQUEST_STATUS_AWAITING_APPROVAL); $result = external::approve_data_request($datarequest->get('id')); $return = (object) \external_api::clean_returnvalue(external::approve_data_request_returns(), $result); $this->assertTrue($return->result); $this->assertEmpty($return->warnings); } /** * Test for external::approve_data_request() for a non-existent request ID. */ public function test_approve_data_request_non_existent() { $this->resetAfterTest(); // Admin as DPO. (The default when no one's assigned as a DPO in the site). $this->setAdminUser(); $result = external::approve_data_request(1); $return = (object) \external_api::clean_returnvalue(external::approve_data_request_returns(), $result); $this->assertFalse($return->result); $this->assertCount(1, $return->warnings); $warning = reset($return->warnings); $this->assertEquals('errorrequestnotfound', $warning['warningcode']); } /** * Test for external::cancel_data_request() of another user. */ public function test_cancel_data_request_other_user() { $this->resetAfterTest(); $generator = new \testing_data_generator(); $requester = $generator->create_user(); $otheruser = $generator->create_user(); $comment = 'sample comment'; // Test data request creation. $this->setUser($requester); $datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment); // Login as other user. $this->setUser($otheruser); $result = external::cancel_data_request($datarequest->get('id')); $return = (object) \external_api::clean_returnvalue(external::approve_data_request_returns(), $result); $this->assertFalse($return->result); $this->assertCount(1, $return->warnings); $warning = reset($return->warnings); $this->assertEquals('errorrequestnotfound', $warning['warningcode']); } /** * Test cancellation of a request where you are the requester of another user's data. */ public function test_cancel_data_request_other_user_as_requester() { $this->resetAfterTest(); $generator = new \testing_data_generator(); $requester = $generator->create_user(); $otheruser = $generator->create_user(); $comment = 'sample comment'; // Assign requester as otheruser'sparent. $systemcontext = \context_system::instance(); $parentrole = $generator->create_role(); assign_capability('tool/dataprivacy:makedatarequestsforchildren', CAP_ALLOW, $parentrole, $systemcontext); role_assign($parentrole, $requester->id, \context_user::instance($otheruser->id)); // Test data request creation. $this->setUser($requester); $datarequest = api::create_data_request($otheruser->id, api::DATAREQUEST_TYPE_EXPORT, $comment); $result = external::cancel_data_request($datarequest->get('id')); $return = (object) \external_api::clean_returnvalue(external::approve_data_request_returns(), $result); $this->assertTrue($return->result); $this->assertEmpty($return->warnings); } /** * Test cancellation of a request where you are the requester of another user's data. */ public function test_cancel_data_request_requester_lost_permissions() { $this->resetAfterTest(); $generator = new \testing_data_generator(); $requester = $generator->create_user(); $otheruser = $generator->create_user(); $comment = 'sample comment'; // Assign requester as otheruser'sparent. $systemcontext = \context_system::instance(); $parentrole = $generator->create_role(); assign_capability('tool/dataprivacy:makedatarequestsforchildren', CAP_ALLOW, $parentrole, $systemcontext); role_assign($parentrole, $requester->id, \context_user::instance($otheruser->id)); $this->setUser($requester); $datarequest = api::create_data_request($otheruser->id, api::DATAREQUEST_TYPE_EXPORT, $comment); // Unassign the role. role_unassign($parentrole, $requester->id, \context_user::instance($otheruser->id)->id); // This user can no longer make the request. $this->expectException(\required_capability_exception::class); $result = external::cancel_data_request($datarequest->get('id')); } /** * Test cancellation of a request where you are the requester of another user's data. */ public function test_cancel_data_request_other_user_as_child() { $this->resetAfterTest(); $generator = new \testing_data_generator(); $requester = $generator->create_user(); $otheruser = $generator->create_user(); $comment = 'sample comment'; // Assign requester as otheruser'sparent. $systemcontext = \context_system::instance(); $parentrole = $generator->create_role(); assign_capability('tool/dataprivacy:makedatarequestsforchildren', CAP_ALLOW, $parentrole, $systemcontext); role_assign($parentrole, $requester->id, \context_user::instance($otheruser->id)); // Test data request creation. $this->setUser($otheruser); $datarequest = api::create_data_request($otheruser->id, api::DATAREQUEST_TYPE_EXPORT, $comment); $result = external::cancel_data_request($datarequest->get('id')); $return = (object) \external_api::clean_returnvalue(external::approve_data_request_returns(), $result); $this->assertTrue($return->result); $this->assertEmpty($return->warnings); } /** * Test for external::cancel_data_request() */ public function test_cancel_data_request() { $this->resetAfterTest(); $generator = new \testing_data_generator(); $requester = $generator->create_user(); $comment = 'sample comment'; // Test data request creation. $this->setUser($requester); $datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment); // Test cancellation. $this->setUser($requester); $result = external::cancel_data_request($datarequest->get('id')); $return = (object) \external_api::clean_returnvalue(external::approve_data_request_returns(), $result); $this->assertTrue($return->result); $this->assertEmpty($return->warnings); } /** * Test contact DPO. */ public function test_contact_dpo() { $this->resetAfterTest(); $generator = new \testing_data_generator(); $user = $generator->create_user(); $this->setUser($user); $message = 'Hello world!'; $result = external::contact_dpo($message); $return = (object) \external_api::clean_returnvalue(external::contact_dpo_returns(), $result); $this->assertTrue($return->result); $this->assertEmpty($return->warnings); } /** * Test contact DPO with message containing invalid input. */ public function test_contact_dpo_with_nasty_input() { $this->resetAfterTest(); $generator = new \testing_data_generator(); $user = $generator->create_user(); $this->setUser($user); $this->expectException('invalid_parameter_exception'); external::contact_dpo('de<>\\..scription'); } /** * Test for external::deny_data_request() with the user not logged in. */ public function test_deny_data_request_not_logged_in() { $this->resetAfterTest(); $generator = new \testing_data_generator(); $requester = $generator->create_user(); $comment = 'sample comment'; // Test data request creation. $this->setUser($requester); $datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment); // Log out. $this->setUser(); $this->expectException(\require_login_exception::class); external::deny_data_request($datarequest->get('id')); } /** * Test for external::deny_data_request() with the user not having a DPO role. */ public function test_deny_data_request_not_dpo() { $this->resetAfterTest(); $generator = new \testing_data_generator(); $requester = $generator->create_user(); $comment = 'sample comment'; $this->setUser($requester); $datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment); // Login as the requester. $this->setUser($requester); $this->expectException(\required_capability_exception::class); external::deny_data_request($datarequest->get('id')); } /** * Test for external::deny_data_request() for request that's not ready for approval */ public function test_deny_data_request_not_waiting_for_approval() { $this->resetAfterTest(); $generator = new \testing_data_generator(); $requester = $generator->create_user(); $comment = 'sample comment'; $this->setUser($requester); $datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment); $datarequest->set('status', api::DATAREQUEST_STATUS_CANCELLED)->save(); // Admin as DPO. (The default when no one's assigned as a DPO in the site). $this->setAdminUser(); $this->expectException(\moodle_exception::class); external::deny_data_request($datarequest->get('id')); } /** * Test for external::deny_data_request() */ public function test_deny_data_request() { $this->resetAfterTest(); $generator = new \testing_data_generator(); $requester = $generator->create_user(); $comment = 'sample comment'; $this->setUser($requester); $datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment); // Admin as DPO. (The default when no one's assigned as a DPO in the site). $this->setAdminUser(); api::update_request_status($datarequest->get('id'), api::DATAREQUEST_STATUS_AWAITING_APPROVAL); $result = external::approve_data_request($datarequest->get('id')); $return = (object) \external_api::clean_returnvalue(external::deny_data_request_returns(), $result); $this->assertTrue($return->result); $this->assertEmpty($return->warnings); } /** * Test for external::deny_data_request() for a non-existent request ID. */ public function test_deny_data_request_non_existent() { $this->resetAfterTest(); // Admin as DPO. (The default when no one's assigned as a DPO in the site). $this->setAdminUser(); $result = external::deny_data_request(1); $return = (object) \external_api::clean_returnvalue(external::deny_data_request_returns(), $result); $this->assertFalse($return->result); $this->assertCount(1, $return->warnings); $warning = reset($return->warnings); $this->assertEquals('errorrequestnotfound', $warning['warningcode']); } /** * Test for external::get_data_request() with the user not logged in. */ public function test_get_data_request_not_logged_in() { $this->resetAfterTest(); $generator = new \testing_data_generator(); $requester = $generator->create_user(); $comment = 'sample comment'; $this->setUser($requester); $datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment); $this->setUser(); $this->expectException(\require_login_exception::class); external::get_data_request($datarequest->get('id')); } /** * Test for external::get_data_request() with the user not having a DPO role. */ public function test_get_data_request_not_dpo() { $this->resetAfterTest(); $generator = new \testing_data_generator(); $requester = $generator->create_user(); $otheruser = $generator->create_user(); $comment = 'sample comment'; $this->setUser($requester); $datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment); // Login as the otheruser. $this->setUser($otheruser); $this->expectException(\required_capability_exception::class); external::get_data_request($datarequest->get('id')); } /** * Test for external::get_data_request() */ public function test_get_data_request() { $this->resetAfterTest(); $generator = new \testing_data_generator(); $requester = $generator->create_user(); $comment = 'sample comment'; $this->setUser($requester); $datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment); // Admin as DPO. (The default when no one's assigned as a DPO in the site). $this->setAdminUser(); $result = external::get_data_request($datarequest->get('id')); $return = (object) \external_api::clean_returnvalue(external::get_data_request_returns(), $result); $this->assertEquals(api::DATAREQUEST_TYPE_EXPORT, $return->result['type']); $this->assertEquals('sample comment', $return->result['comments']); $this->assertEquals($requester->id, $return->result['userid']); $this->assertEquals($requester->id, $return->result['requestedby']); $this->assertEmpty($return->warnings); } /** * Test for external::get_data_request() for a non-existent request ID. */ public function test_get_data_request_non_existent() { $this->resetAfterTest(); // Admin as DPO. (The default when no one's assigned as a DPO in the site). $this->setAdminUser(); $this->expectException(\dml_missing_record_exception::class); external::get_data_request(1); } /** * Test for \tool_dataprivacy\external::set_context_defaults() * when called by a user that doesn't have the manage registry capability. */ public function test_set_context_defaults_no_capability() { $this->resetAfterTest(); $generator = $this->getDataGenerator(); $user = $generator->create_user(); $this->setUser($user); $this->expectException(\required_capability_exception::class); external::set_context_defaults(CONTEXT_COURSECAT, context_instance::INHERIT, context_instance::INHERIT, '', false); } /** * Test for \tool_dataprivacy\external::set_context_defaults(). * * We're just checking the module context level here to test the WS function. * More testing is done in \tool_dataprivacy_api_testcase::test_set_context_defaults(). * * @dataProvider get_options_provider * @param bool $modulelevel Whether defaults are to be applied on the module context level or for an activity only. * @param bool $override Whether to override instances. */ public function test_set_context_defaults($modulelevel, $override) { $this->resetAfterTest(); $this->setAdminUser(); $generator = $this->getDataGenerator(); // Generate course cat, course, block, assignment, forum instances. $coursecat = $generator->create_category(); $course = $generator->create_course(['category' => $coursecat->id]); $assign = $generator->create_module('assign', ['course' => $course->id]); list($course, $assigncm) = get_course_and_cm_from_instance($assign->id, 'assign'); $assigncontext = \context_module::instance($assigncm->id); // Generate purpose and category. $category1 = api::create_category((object)['name' => 'Test category 1']); $category2 = api::create_category((object)['name' => 'Test category 2']); $purpose1 = api::create_purpose((object)[ 'name' => 'Test purpose 1', 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a' ]); $purpose2 = api::create_purpose((object)[ 'name' => 'Test purpose 2', 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a' ]); // Set a custom purpose and ID for this assignment instance. $assignctxinstance = api::set_context_instance((object) [ 'contextid' => $assigncontext->id, 'purposeid' => $purpose1->get('id'), 'categoryid' => $category1->get('id'), ]); $modulename = $modulelevel ? 'assign' : ''; $categoryid = $category2->get('id'); $purposeid = $purpose2->get('id'); $result = external::set_context_defaults(CONTEXT_MODULE, $categoryid, $purposeid, $modulename, $override); // Extract the result. $return = \external_api::clean_returnvalue(external::set_context_defaults_returns(), $result); $this->assertTrue($return['result']); // Check the assignment context instance. $instanceexists = context_instance::record_exists($assignctxinstance->get('id')); if ($override) { // The custom assign instance should have been deleted. $this->assertFalse($instanceexists); } else { // The custom assign instance should still exist. $this->assertTrue($instanceexists); } // Check the saved defaults. list($savedpurpose, $savedcategory) = \tool_dataprivacy\data_registry::get_defaults(CONTEXT_MODULE, $modulename); $this->assertEquals($categoryid, $savedcategory); $this->assertEquals($purposeid, $savedpurpose); } /** * Test for \tool_dataprivacy\external::get_category_options() * when called by a user that doesn't have the manage registry capability. */ public function test_get_category_options_no_capability() { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $this->expectException(\required_capability_exception::class); external::get_category_options(true, true); } /** * Data provider for \tool_dataprivacy_external_testcase::test_XX_options(). */ public function get_options_provider() { return [ [false, false], [false, true], [true, false], [true, true], ]; } /** * Test for \tool_dataprivacy\external::get_category_options(). * * @dataProvider get_options_provider * @param bool $includeinherit Whether "Inherit" would be included to the options. * @param bool $includenotset Whether "Not set" would be included to the options. */ public function test_get_category_options($includeinherit, $includenotset) { $this->resetAfterTest(); $this->setAdminUser(); // Prepare our expected options. $expectedoptions = []; if ($includeinherit) { $expectedoptions[] = [ 'id' => context_instance::INHERIT, 'name' => get_string('inherit', 'tool_dataprivacy'), ]; } if ($includenotset) { $expectedoptions[] = [ 'id' => context_instance::NOTSET, 'name' => get_string('notset', 'tool_dataprivacy'), ]; } for ($i = 1; $i <= 3; $i++) { $category = api::create_category((object)['name' => 'Category ' . $i]); $expectedoptions[] = [ 'id' => $category->get('id'), 'name' => $category->get('name'), ]; } // Call the WS function. $result = external::get_category_options($includeinherit, $includenotset); // Extract the options. $return = (object) \external_api::clean_returnvalue(external::get_category_options_returns(), $result); $options = $return->options; // Make sure everything checks out. $this->assertCount(count($expectedoptions), $options); foreach ($options as $option) { $this->assertContains($option, $expectedoptions); } } /** * Test for \tool_dataprivacy\external::get_purpose_options() * when called by a user that doesn't have the manage registry capability. */ public function test_get_purpose_options_no_capability() { $this->resetAfterTest(); $generator = $this->getDataGenerator(); $user = $generator->create_user(); $this->setUser($user); $this->expectException(\required_capability_exception::class); external::get_category_options(true, true); } /** * Test for \tool_dataprivacy\external::get_purpose_options(). * * @dataProvider get_options_provider * @param bool $includeinherit Whether "Inherit" would be included to the options. * @param bool $includenotset Whether "Not set" would be included to the options. */ public function test_get_purpose_options($includeinherit, $includenotset) { $this->resetAfterTest(); $this->setAdminUser(); // Prepare our expected options. $expectedoptions = []; if ($includeinherit) { $expectedoptions[] = [ 'id' => context_instance::INHERIT, 'name' => get_string('inherit', 'tool_dataprivacy'), ]; } if ($includenotset) { $expectedoptions[] = [ 'id' => context_instance::NOTSET, 'name' => get_string('notset', 'tool_dataprivacy'), ]; } for ($i = 1; $i <= 3; $i++) { $purpose = api::create_purpose((object)[ 'name' => 'Purpose ' . $i, 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a' ]); $expectedoptions[] = [ 'id' => $purpose->get('id'), 'name' => $purpose->get('name'), ]; } // Call the WS function. $result = external::get_purpose_options($includeinherit, $includenotset); // Extract the options. $return = (object) \external_api::clean_returnvalue(external::get_purpose_options_returns(), $result); $options = $return->options; // Make sure everything checks out. $this->assertCount(count($expectedoptions), $options); foreach ($options as $option) { $this->assertContains($option, $expectedoptions); } } /** * Data provider for \tool_dataprivacy_external_testcase::get_activity_options(). */ public function get_activity_options_provider() { return [ [false, false, true], [false, true, true], [true, false, true], [true, true, true], [false, false, false], [false, true, false], [true, false, false], [true, true, false], ]; } /** * Test for \tool_dataprivacy\external::get_activity_options(). * * @dataProvider get_activity_options_provider * @param bool $inheritcategory Whether the category would be set to "Inherit". * @param bool $inheritpurpose Whether the purpose would be set to "Inherit". * @param bool $nodefaults Whether to fetch only activities that don't have defaults. */ public function test_get_activity_options($inheritcategory, $inheritpurpose, $nodefaults) { $this->resetAfterTest(); $this->setAdminUser(); $category = api::create_category((object)['name' => 'Test category']); $purpose = api::create_purpose((object)[ 'name' => 'Test purpose ', 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a' ]); $categoryid = $category->get('id'); $purposeid = $purpose->get('id'); if ($inheritcategory) { $categoryid = context_instance::INHERIT; } if ($inheritpurpose) { $purposeid = context_instance::INHERIT; } // Set the context default for the assignment module. api::set_context_defaults(CONTEXT_MODULE, $categoryid, $purposeid, 'assign'); // Call the WS function. $result = external::get_activity_options($nodefaults); // Extract the options. $return = (object) \external_api::clean_returnvalue(external::get_activity_options_returns(), $result); $options = $return->options; // Make sure the options list is not empty. $this->assertNotEmpty($options); $pluginwithdefaults = [ 'name' => 'assign', 'displayname' => get_string('pluginname', 'assign') ]; // If we don't want plugins with defaults to be listed or if both of the category and purpose are set to inherit, // the assign module should be listed. if (!$nodefaults || ($inheritcategory && $inheritpurpose)) { $this->assertContains($pluginwithdefaults, $options); } else { $this->assertNotContains($pluginwithdefaults, $options); } } /** * Test for external::bulk_approve_data_requests(). */ public function test_bulk_approve_data_requests() { $this->resetAfterTest(); // Create delete data requests. $requester1 = $this->getDataGenerator()->create_user(); $this->setUser($requester1->id); $datarequest1 = api::create_data_request($requester1->id, api::DATAREQUEST_TYPE_DELETE, 'Example comment'); $requestid1 = $datarequest1->get('id'); $requester2 = $this->getDataGenerator()->create_user(); $this->setUser($requester2->id); $datarequest2 = api::create_data_request($requester2->id, api::DATAREQUEST_TYPE_DELETE, 'Example comment'); $requestid2 = $datarequest2->get('id'); // Approve the requests. $this->setAdminUser(); api::update_request_status($requestid1, api::DATAREQUEST_STATUS_AWAITING_APPROVAL); api::update_request_status($requestid2, api::DATAREQUEST_STATUS_AWAITING_APPROVAL); $result = external::bulk_approve_data_requests([$requestid1, $requestid2]); $return = (object) \external_api::clean_returnvalue(external::bulk_approve_data_requests_returns(), $result); $this->assertTrue($return->result); $this->assertEmpty($return->warnings); } /** * Test for external::bulk_approve_data_requests() for a non-existent request ID. */ public function test_bulk_approve_data_requests_non_existent() { $this->resetAfterTest(); $this->setAdminUser(); $result = external::bulk_approve_data_requests([42]); $return = (object) \external_api::clean_returnvalue(external::bulk_approve_data_requests_returns(), $result); $this->assertFalse($return->result); $this->assertCount(1, $return->warnings); $warning = reset($return->warnings); $this->assertEquals('errorrequestnotfound', $warning['warningcode']); $this->assertEquals(42, $warning['item']); } /** * Test for external::bulk_deny_data_requests() for a user without permission to deny requests. */ public function test_bulk_approve_data_requests_no_permission() { $this->resetAfterTest(); // Create delete data requests. $requester1 = $this->getDataGenerator()->create_user(); $this->setUser($requester1->id); $datarequest1 = api::create_data_request($requester1->id, api::DATAREQUEST_TYPE_DELETE, 'Example comment'); $requestid1 = $datarequest1->get('id'); $requester2 = $this->getDataGenerator()->create_user(); $this->setUser($requester2->id); $datarequest2 = api::create_data_request($requester2->id, api::DATAREQUEST_TYPE_DELETE, 'Example comment'); $requestid2 = $datarequest2->get('id'); $this->setAdminUser(); api::update_request_status($requestid1, api::DATAREQUEST_STATUS_AWAITING_APPROVAL); api::update_request_status($requestid2, api::DATAREQUEST_STATUS_AWAITING_APPROVAL); // Approve the requests. $uut = $this->getDataGenerator()->create_user(); $this->setUser($uut); $this->expectException(\required_capability_exception::class); $result = external::bulk_approve_data_requests([$requestid1, $requestid2]); } /** * Test for external::bulk_deny_data_requests() for a user without permission to deny requests. */ public function test_bulk_approve_data_requests_own_request() { $this->resetAfterTest(); // Create delete data requests. $requester1 = $this->getDataGenerator()->create_user(); $this->setUser($requester1->id); $datarequest1 = api::create_data_request($requester1->id, api::DATAREQUEST_TYPE_DELETE, 'Example comment'); $requestid1 = $datarequest1->get('id'); $requester2 = $this->getDataGenerator()->create_user(); $this->setUser($requester2->id); $datarequest2 = api::create_data_request($requester2->id, api::DATAREQUEST_TYPE_DELETE, 'Example comment'); $requestid2 = $datarequest2->get('id'); $this->setAdminUser(); api::update_request_status($requestid1, api::DATAREQUEST_STATUS_AWAITING_APPROVAL); api::update_request_status($requestid2, api::DATAREQUEST_STATUS_AWAITING_APPROVAL); // Deny the requests. $this->setUser($requester1); $this->expectException(\required_capability_exception::class); $result = external::bulk_approve_data_requests([$requestid1]); } /** * Test for external::bulk_deny_data_requests(). */ public function test_bulk_deny_data_requests() { $this->resetAfterTest(); // Create delete data requests. $requester1 = $this->getDataGenerator()->create_user(); $this->setUser($requester1->id); $datarequest1 = api::create_data_request($requester1->id, api::DATAREQUEST_TYPE_DELETE, 'Example comment'); $requestid1 = $datarequest1->get('id'); $requester2 = $this->getDataGenerator()->create_user(); $this->setUser($requester2->id); $datarequest2 = api::create_data_request($requester2->id, api::DATAREQUEST_TYPE_DELETE, 'Example comment'); $requestid2 = $datarequest2->get('id'); // Deny the requests. $this->setAdminUser(); api::update_request_status($requestid1, api::DATAREQUEST_STATUS_AWAITING_APPROVAL); api::update_request_status($requestid2, api::DATAREQUEST_STATUS_AWAITING_APPROVAL); $result = external::bulk_deny_data_requests([$requestid1, $requestid2]); $return = (object) \external_api::clean_returnvalue(external::bulk_approve_data_requests_returns(), $result); $this->assertTrue($return->result); $this->assertEmpty($return->warnings); } /** * Test for external::bulk_deny_data_requests() for a non-existent request ID. */ public function test_bulk_deny_data_requests_non_existent() { $this->resetAfterTest(); $this->setAdminUser(); $result = external::bulk_deny_data_requests([42]); $return = (object) \external_api::clean_returnvalue(external::bulk_approve_data_requests_returns(), $result); $this->assertFalse($return->result); $this->assertCount(1, $return->warnings); $warning = reset($return->warnings); $this->assertEquals('errorrequestnotfound', $warning['warningcode']); $this->assertEquals(42, $warning['item']); } /** * Test for external::bulk_deny_data_requests() for a user without permission to deny requests. */ public function test_bulk_deny_data_requests_no_permission() { $this->resetAfterTest(); // Create delete data requests. $requester1 = $this->getDataGenerator()->create_user(); $this->setUser($requester1->id); $datarequest1 = api::create_data_request($requester1->id, api::DATAREQUEST_TYPE_DELETE, 'Example comment'); $requestid1 = $datarequest1->get('id'); $requester2 = $this->getDataGenerator()->create_user(); $this->setUser($requester2->id); $datarequest2 = api::create_data_request($requester2->id, api::DATAREQUEST_TYPE_DELETE, 'Example comment'); $requestid2 = $datarequest2->get('id'); $this->setAdminUser(); api::update_request_status($requestid1, api::DATAREQUEST_STATUS_AWAITING_APPROVAL); api::update_request_status($requestid2, api::DATAREQUEST_STATUS_AWAITING_APPROVAL); // Deny the requests. $uut = $this->getDataGenerator()->create_user(); $this->setUser($uut); $this->expectException(\required_capability_exception::class); $result = external::bulk_deny_data_requests([$requestid1, $requestid2]); } /** * Test for external::bulk_deny_data_requests() for a user cannot approve their own request. */ public function test_bulk_deny_data_requests_own_request() { $this->resetAfterTest(); // Create delete data requests. $requester1 = $this->getDataGenerator()->create_user(); $this->setUser($requester1->id); $datarequest1 = api::create_data_request($requester1->id, api::DATAREQUEST_TYPE_DELETE, 'Example comment'); $requestid1 = $datarequest1->get('id'); $requester2 = $this->getDataGenerator()->create_user(); $this->setUser($requester2->id); $datarequest2 = api::create_data_request($requester2->id, api::DATAREQUEST_TYPE_DELETE, 'Example comment'); $requestid2 = $datarequest2->get('id'); $this->setAdminUser(); api::update_request_status($requestid1, api::DATAREQUEST_STATUS_AWAITING_APPROVAL); api::update_request_status($requestid2, api::DATAREQUEST_STATUS_AWAITING_APPROVAL); // Deny the requests. $this->setUser($requester1); $this->expectException(\required_capability_exception::class); $result = external::bulk_deny_data_requests([$requestid1]); } /** * Test for external::get_users(), case search using non-identity field without * facing any permission problem. * * @throws coding_exception * @throws dml_exception * @throws invalid_parameter_exception * @throws required_capability_exception * @throws restricted_context_exception */ public function test_get_users_using_using_non_identity() { $this->resetAfterTest(); $context = \context_system::instance(); $requester = $this->getDataGenerator()->create_user(); $role = $this->getDataGenerator()->create_role(); role_assign($role, $requester->id, $context); assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $role, $context); $this->setUser($requester); $this->getDataGenerator()->create_user([ 'firstname' => 'First Student' ]); $student2 = $this->getDataGenerator()->create_user([ 'firstname' => 'Second Student' ]); $results = external::get_users('Second'); $this->assertCount(1, $results); $this->assertEquals((object)[ 'id' => $student2->id, 'fullname' => fullname($student2), 'extrafields' => [] ], $results[$student2->id]); } /** * Test for external::get_users(), case search using identity field but * don't have "moodle/site:viewuseridentity" permission. * * @throws coding_exception * @throws dml_exception * @throws invalid_parameter_exception * @throws required_capability_exception * @throws restricted_context_exception */ public function test_get_users_using_identity_without_permission() { global $CFG; $this->resetAfterTest(); $CFG->showuseridentity = 'institution'; // Create requester user and assign correct capability. $context = \context_system::instance(); $requester = $this->getDataGenerator()->create_user(); $role = $this->getDataGenerator()->create_role(); role_assign($role, $requester->id, $context); assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $role, $context); $this->setUser($requester); $this->getDataGenerator()->create_user([ 'institution' => 'University1' ]); $results = external::get_users('University1'); $this->assertEmpty($results); } /** * Test for external::get_users(), case search using disabled identity field * even they have "moodle/site:viewuseridentity" permission. * * @throws coding_exception * @throws dml_exception * @throws invalid_parameter_exception * @throws required_capability_exception * @throws restricted_context_exception */ public function test_get_users_using_field_not_in_identity() { $this->resetAfterTest(); $context = \context_system::instance(); $requester = $this->getDataGenerator()->create_user(); $role = $this->getDataGenerator()->create_role(); role_assign($role, $requester->id, $context); assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $role, $context); assign_capability('moodle/site:viewuseridentity', CAP_ALLOW, $role, $context); $this->setUser($requester); $this->getDataGenerator()->create_user([ 'institution' => 'University1' ]); $results = external::get_users('University1'); $this->assertEmpty($results); } /** * Test for external::get_users(), case search using enabled identity field * with "moodle/site:viewuseridentity" permission. * * @throws coding_exception * @throws dml_exception * @throws invalid_parameter_exception * @throws required_capability_exception * @throws restricted_context_exception */ public function test_get_users() { global $CFG; $this->resetAfterTest(); $CFG->showuseridentity = 'institution'; $context = \context_system::instance(); $requester = $this->getDataGenerator()->create_user(); $role = $this->getDataGenerator()->create_role(); role_assign($role, $requester->id, $context); assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $role, $context); assign_capability('moodle/site:viewuseridentity', CAP_ALLOW, $role, $context); $this->setUser($requester); $student1 = $this->getDataGenerator()->create_user([ 'institution' => 'University1' ]); $this->getDataGenerator()->create_user([ 'institution' => 'University2' ]); $results = external::get_users('University1'); $this->assertCount(1, $results); $this->assertEquals((object)[ 'id' => $student1->id, 'fullname' => fullname($student1), 'extrafields' => [ 0 => (object)[ 'name' => 'institution', 'value' => 'University1' ] ] ], $results[$student1->id]); } } tests/api_test.php 0000644 00000334476 15152701722 0010252 0 ustar 00 <?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/>. namespace tool_dataprivacy; use core\invalid_persistent_exception; use core\task\manager; use testing_data_generator; use tool_dataprivacy\local\helper; use tool_dataprivacy\task\process_data_request_task; /** * API tests. * * @package tool_dataprivacy * @covers \tool_dataprivacy\api * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class api_test extends \advanced_testcase { /** * Ensure that the check_can_manage_data_registry function fails cap testing when a user without capabilities is * tested with the default context. */ public function test_check_can_manage_data_registry_admin() { $this->resetAfterTest(); $this->setAdminUser(); // Technically this actually returns void, but assertNull will suffice to avoid a pointless test. $this->assertNull(api::check_can_manage_data_registry()); } /** * Ensure that the check_can_manage_data_registry function fails cap testing when a user without capabilities is * tested with the default context. */ public function test_check_can_manage_data_registry_without_cap_default() { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $this->expectException(\required_capability_exception::class); api::check_can_manage_data_registry(); } /** * Ensure that the check_can_manage_data_registry function fails cap testing when a user without capabilities is * tested with the default context. */ public function test_check_can_manage_data_registry_without_cap_system() { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $this->expectException(\required_capability_exception::class); api::check_can_manage_data_registry(\context_system::instance()->id); } /** * Ensure that the check_can_manage_data_registry function fails cap testing when a user without capabilities is * tested with the default context. */ public function test_check_can_manage_data_registry_without_cap_own_user() { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $this->expectException(\required_capability_exception::class); api::check_can_manage_data_registry(\context_user::instance($user->id)->id); } /** * Test for api::update_request_status(). */ public function test_update_request_status() { $this->resetAfterTest(); $generator = new testing_data_generator(); $s1 = $generator->create_user(); $this->setUser($s1); // Create the sample data request. $datarequest = api::create_data_request($s1->id, api::DATAREQUEST_TYPE_EXPORT); $requestid = $datarequest->get('id'); // Update with a comment. $comment = 'This is an example of a comment'; $result = api::update_request_status($requestid, api::DATAREQUEST_STATUS_AWAITING_APPROVAL, 0, $comment); $this->assertTrue($result); $datarequest = new data_request($requestid); $this->assertStringEndsWith($comment, $datarequest->get('dpocomment')); // Update with a comment which will be trimmed. $result = api::update_request_status($requestid, api::DATAREQUEST_STATUS_AWAITING_APPROVAL, 0, ' '); $this->assertTrue($result); $datarequest = new data_request($requestid); $this->assertStringEndsWith($comment, $datarequest->get('dpocomment')); // Update with a comment. $secondcomment = ' - More comments - '; $result = api::update_request_status($requestid, api::DATAREQUEST_STATUS_AWAITING_APPROVAL, 0, $secondcomment); $this->assertTrue($result); $datarequest = new data_request($requestid); $this->assertMatchesRegularExpression("/.*{$comment}.*{$secondcomment}/s", $datarequest->get('dpocomment')); // Update with a valid status. $result = api::update_request_status($requestid, api::DATAREQUEST_STATUS_DOWNLOAD_READY); $this->assertTrue($result); // Fetch the request record again. $datarequest = new data_request($requestid); $this->assertEquals(api::DATAREQUEST_STATUS_DOWNLOAD_READY, $datarequest->get('status')); // Update with an invalid status. $this->expectException(invalid_persistent_exception::class); api::update_request_status($requestid, -1); } /** * Test for api::get_site_dpos() when there are no users with the DPO role. */ public function test_get_site_dpos_no_dpos() { $this->resetAfterTest(); $admin = get_admin(); $dpos = api::get_site_dpos(); $this->assertCount(1, $dpos); $dpo = reset($dpos); $this->assertEquals($admin->id, $dpo->id); } /** * Test for api::get_site_dpos() when there are no users with the DPO role. */ public function test_get_site_dpos() { global $DB; $this->resetAfterTest(); $generator = new testing_data_generator(); $u1 = $generator->create_user(); $u2 = $generator->create_user(); $context = \context_system::instance(); // Give the manager role with the capability to manage data requests. $managerroleid = $DB->get_field('role', 'id', array('shortname' => 'manager')); assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $managerroleid, $context->id, true); // Assign u1 as a manager. role_assign($managerroleid, $u1->id, $context->id); // Give the editing teacher role with the capability to manage data requests. $editingteacherroleid = $DB->get_field('role', 'id', array('shortname' => 'editingteacher')); assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $editingteacherroleid, $context->id, true); // Assign u1 as an editing teacher as well. role_assign($editingteacherroleid, $u1->id, $context->id); // Assign u2 as an editing teacher. role_assign($editingteacherroleid, $u2->id, $context->id); // Only map the manager role to the DPO role. set_config('dporoles', $managerroleid, 'tool_dataprivacy'); $dpos = api::get_site_dpos(); $this->assertCount(1, $dpos); $dpo = reset($dpos); $this->assertEquals($u1->id, $dpo->id); } /** * Test for \tool_dataprivacy\api::get_assigned_privacy_officer_roles(). */ public function test_get_assigned_privacy_officer_roles() { global $DB; $this->resetAfterTest(); // Erroneously set the manager roles as the PO, even if it doesn't have the managedatarequests capability yet. $managerroleid = $DB->get_field('role', 'id', array('shortname' => 'manager')); set_config('dporoles', $managerroleid, 'tool_dataprivacy'); // Get the assigned PO roles when nothing has been set yet. $roleids = api::get_assigned_privacy_officer_roles(); // Confirm that the returned list is empty. $this->assertEmpty($roleids); $context = \context_system::instance(); // Give the manager role with the capability to manage data requests. assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $managerroleid, $context->id, true); // Give the editing teacher role with the capability to manage data requests. $editingteacherroleid = $DB->get_field('role', 'id', array('shortname' => 'editingteacher')); assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $editingteacherroleid, $context->id, true); // Get the non-editing teacher role ID. $teacherroleid = $DB->get_field('role', 'id', array('shortname' => 'teacher')); // Erroneously map the manager and the non-editing teacher roles to the PO role. $badconfig = $managerroleid . ',' . $teacherroleid; set_config('dporoles', $badconfig, 'tool_dataprivacy'); // Get the assigned PO roles. $roleids = api::get_assigned_privacy_officer_roles(); // There should only be one PO role. $this->assertCount(1, $roleids); // Confirm it contains the manager role. $this->assertContainsEquals($managerroleid, $roleids); // And it does not contain the editing teacher role. $this->assertNotContainsEquals($editingteacherroleid, $roleids); } /** * Test for api::approve_data_request(). */ public function test_approve_data_request() { global $DB; $this->resetAfterTest(); $generator = new testing_data_generator(); $s1 = $generator->create_user(); $u1 = $generator->create_user(); $context = \context_system::instance(); // Manager role. $managerroleid = $DB->get_field('role', 'id', array('shortname' => 'manager')); // Give the manager role with the capability to manage data requests. assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $managerroleid, $context->id, true); // Assign u1 as a manager. role_assign($managerroleid, $u1->id, $context->id); // Map the manager role to the DPO role. set_config('dporoles', $managerroleid, 'tool_dataprivacy'); // Create the sample data request. $this->setUser($s1); $datarequest = api::create_data_request($s1->id, api::DATAREQUEST_TYPE_EXPORT); $requestid = $datarequest->get('id'); // Make this ready for approval. api::update_request_status($requestid, api::DATAREQUEST_STATUS_AWAITING_APPROVAL); $this->setUser($u1); $result = api::approve_data_request($requestid); $this->assertTrue($result); $datarequest = new data_request($requestid); $this->assertEquals($u1->id, $datarequest->get('dpo')); $this->assertEquals(api::DATAREQUEST_STATUS_APPROVED, $datarequest->get('status')); // Test adhoc task creation. $adhoctasks = manager::get_adhoc_tasks(process_data_request_task::class); $this->assertCount(1, $adhoctasks); } /** * Test for api::approve_data_request() when called by a user who doesn't have the DPO role. */ public function test_approve_data_request_non_dpo_user() { $this->resetAfterTest(); $generator = new testing_data_generator(); $student = $generator->create_user(); $teacher = $generator->create_user(); // Create the sample data request. $this->setUser($student); $datarequest = api::create_data_request($student->id, api::DATAREQUEST_TYPE_EXPORT); $requestid = $datarequest->get('id'); // Login as a user without DPO role. $this->setUser($teacher); $this->expectException(\required_capability_exception::class); api::approve_data_request($requestid); } /** * Test that deletion requests for the primary admin are rejected */ public function test_reject_data_deletion_request_primary_admin() { $this->resetAfterTest(); $this->setAdminUser(); $datarequest = api::create_data_request(get_admin()->id, api::DATAREQUEST_TYPE_DELETE); // Approve the request and execute the ad-hoc process task. ob_start(); api::approve_data_request($datarequest->get('id')); $this->runAdhocTasks('\tool_dataprivacy\task\process_data_request_task'); ob_end_clean(); $request = api::get_request($datarequest->get('id')); $this->assertEquals(api::DATAREQUEST_STATUS_REJECTED, $request->get('status')); // Confirm they weren't deleted. $user = \core_user::get_user($request->get('userid')); \core_user::require_active_user($user); } /** * Test for api::can_contact_dpo() */ public function test_can_contact_dpo() { $this->resetAfterTest(); // Default ('contactdataprotectionofficer' is disabled by default). $this->assertFalse(api::can_contact_dpo()); // Enable. set_config('contactdataprotectionofficer', 1, 'tool_dataprivacy'); $this->assertTrue(api::can_contact_dpo()); // Disable again. set_config('contactdataprotectionofficer', 0, 'tool_dataprivacy'); $this->assertFalse(api::can_contact_dpo()); } /** * Test for api::can_manage_data_requests() */ public function test_can_manage_data_requests() { global $DB; $this->resetAfterTest(); // No configured site DPOs yet. $admin = get_admin(); $this->assertTrue(api::can_manage_data_requests($admin->id)); $generator = new testing_data_generator(); $dpo = $generator->create_user(); $nondpocapable = $generator->create_user(); $nondpoincapable = $generator->create_user(); $context = \context_system::instance(); // Manager role. $managerroleid = $DB->get_field('role', 'id', array('shortname' => 'manager')); // Give the manager role with the capability to manage data requests. assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $managerroleid, $context->id, true); // Assign u1 as a manager. role_assign($managerroleid, $dpo->id, $context->id); // Editing teacher role. $editingteacherroleid = $DB->get_field('role', 'id', array('shortname' => 'editingteacher')); // Give the editing teacher role with the capability to manage data requests. assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $managerroleid, $context->id, true); // Assign u2 as an editing teacher. role_assign($editingteacherroleid, $nondpocapable->id, $context->id); // Map only the manager role to the DPO role. set_config('dporoles', $managerroleid, 'tool_dataprivacy'); // User with capability and has DPO role. $this->assertTrue(api::can_manage_data_requests($dpo->id)); // User with capability but has no DPO role. $this->assertFalse(api::can_manage_data_requests($nondpocapable->id)); // User without the capability and has no DPO role. $this->assertFalse(api::can_manage_data_requests($nondpoincapable->id)); } /** * Test that a user who has no capability to make any data requests for children cannot create data requests for any * other user. */ public function test_can_create_data_request_for_user_no() { $this->resetAfterTest(); $parent = $this->getDataGenerator()->create_user(); $otheruser = $this->getDataGenerator()->create_user(); $this->setUser($parent); $this->assertFalse(api::can_create_data_request_for_user($otheruser->id)); } /** * Test that a user who has the capability to make any data requests for one other user cannot create data requests * for any other user. */ public function test_can_create_data_request_for_user_some() { $this->resetAfterTest(); $parent = $this->getDataGenerator()->create_user(); $child = $this->getDataGenerator()->create_user(); $otheruser = $this->getDataGenerator()->create_user(); $systemcontext = \context_system::instance(); $parentrole = $this->getDataGenerator()->create_role(); assign_capability('tool/dataprivacy:makedatarequestsforchildren', CAP_ALLOW, $parentrole, $systemcontext); role_assign($parentrole, $parent->id, \context_user::instance($child->id)); $this->setUser($parent); $this->assertFalse(api::can_create_data_request_for_user($otheruser->id)); } /** * Test that a user who has the capability to make any data requests for one other user cannot create data requests * for any other user. */ public function test_can_create_data_request_for_user_own_child() { $this->resetAfterTest(); $parent = $this->getDataGenerator()->create_user(); $child = $this->getDataGenerator()->create_user(); $systemcontext = \context_system::instance(); $parentrole = $this->getDataGenerator()->create_role(); assign_capability('tool/dataprivacy:makedatarequestsforchildren', CAP_ALLOW, $parentrole, $systemcontext); role_assign($parentrole, $parent->id, \context_user::instance($child->id)); $this->setUser($parent); $this->assertTrue(api::can_create_data_request_for_user($child->id)); } /** * Test that a user who has no capability to make any data requests for children cannot create data requests for any * other user. */ public function test_require_can_create_data_request_for_user_no() { $this->resetAfterTest(); $parent = $this->getDataGenerator()->create_user(); $otheruser = $this->getDataGenerator()->create_user(); $this->setUser($parent); $this->expectException('required_capability_exception'); api::require_can_create_data_request_for_user($otheruser->id); } /** * Test that a user who has the capability to make any data requests for one other user cannot create data requests * for any other user. */ public function test_require_can_create_data_request_for_user_some() { $this->resetAfterTest(); $parent = $this->getDataGenerator()->create_user(); $child = $this->getDataGenerator()->create_user(); $otheruser = $this->getDataGenerator()->create_user(); $systemcontext = \context_system::instance(); $parentrole = $this->getDataGenerator()->create_role(); assign_capability('tool/dataprivacy:makedatarequestsforchildren', CAP_ALLOW, $parentrole, $systemcontext); role_assign($parentrole, $parent->id, \context_user::instance($child->id)); $this->setUser($parent); $this->expectException('required_capability_exception'); api::require_can_create_data_request_for_user($otheruser->id); } /** * Test that a user who has the capability to make any data requests for one other user cannot create data requests * for any other user. */ public function test_require_can_create_data_request_for_user_own_child() { $this->resetAfterTest(); $parent = $this->getDataGenerator()->create_user(); $child = $this->getDataGenerator()->create_user(); $systemcontext = \context_system::instance(); $parentrole = $this->getDataGenerator()->create_role(); assign_capability('tool/dataprivacy:makedatarequestsforchildren', CAP_ALLOW, $parentrole, $systemcontext); role_assign($parentrole, $parent->id, \context_user::instance($child->id)); $this->setUser($parent); $this->assertTrue(api::require_can_create_data_request_for_user($child->id)); } /** * Test for api::can_download_data_request_for_user() */ public function test_can_download_data_request_for_user() { $this->resetAfterTest(); $generator = $this->getDataGenerator(); // Three victims. $victim1 = $generator->create_user(); $victim2 = $generator->create_user(); $victim3 = $generator->create_user(); // Assign a user as victim 1's parent. $systemcontext = \context_system::instance(); $parentrole = $generator->create_role(); assign_capability('tool/dataprivacy:makedatarequestsforchildren', CAP_ALLOW, $parentrole, $systemcontext); $parent = $generator->create_user(); role_assign($parentrole, $parent->id, \context_user::instance($victim1->id)); // Assign another user as data access wonder woman. $wonderrole = $generator->create_role(); assign_capability('tool/dataprivacy:downloadallrequests', CAP_ALLOW, $wonderrole, $systemcontext); $staff = $generator->create_user(); role_assign($wonderrole, $staff->id, $systemcontext); // Finally, victim 3 has been naughty; stop them accessing their own data. $naughtyrole = $generator->create_role(); assign_capability('tool/dataprivacy:downloadownrequest', CAP_PROHIBIT, $naughtyrole, $systemcontext); role_assign($naughtyrole, $victim3->id, $systemcontext); // Victims 1 and 2 can access their own data, regardless of who requested it. $this->assertTrue(api::can_download_data_request_for_user($victim1->id, $victim1->id, $victim1->id)); $this->assertTrue(api::can_download_data_request_for_user($victim2->id, $staff->id, $victim2->id)); // Victim 3 cannot access his own data. $this->assertFalse(api::can_download_data_request_for_user($victim3->id, $victim3->id, $victim3->id)); // Victims 1 and 2 cannot access another victim's data. $this->assertFalse(api::can_download_data_request_for_user($victim2->id, $victim1->id, $victim1->id)); $this->assertFalse(api::can_download_data_request_for_user($victim1->id, $staff->id, $victim2->id)); // Staff can access everyone's data. $this->assertTrue(api::can_download_data_request_for_user($victim1->id, $victim1->id, $staff->id)); $this->assertTrue(api::can_download_data_request_for_user($victim2->id, $staff->id, $staff->id)); $this->assertTrue(api::can_download_data_request_for_user($victim3->id, $staff->id, $staff->id)); // Parent can access victim 1's data only if they requested it. $this->assertTrue(api::can_download_data_request_for_user($victim1->id, $parent->id, $parent->id)); $this->assertFalse(api::can_download_data_request_for_user($victim1->id, $staff->id, $parent->id)); $this->assertFalse(api::can_download_data_request_for_user($victim2->id, $parent->id, $parent->id)); } /** * Data provider for data request creation tests. * * @return array */ public function data_request_creation_provider() { return [ 'Export request by user, automatic approval off' => [ false, api::DATAREQUEST_TYPE_EXPORT, 'automaticdataexportapproval', false, 0, api::DATAREQUEST_STATUS_AWAITING_APPROVAL, 0 ], 'Export request by user, automatic approval on' => [ false, api::DATAREQUEST_TYPE_EXPORT, 'automaticdataexportapproval', true, 0, api::DATAREQUEST_STATUS_APPROVED, 1 ], 'Export request by PO, automatic approval off' => [ true, api::DATAREQUEST_TYPE_EXPORT, 'automaticdataexportapproval', false, 0, api::DATAREQUEST_STATUS_AWAITING_APPROVAL, 0 ], 'Export request by PO, automatic approval on' => [ true, api::DATAREQUEST_TYPE_EXPORT, 'automaticdataexportapproval', true, 'dpo', api::DATAREQUEST_STATUS_APPROVED, 1 ], 'Delete request by user, automatic approval off' => [ false, api::DATAREQUEST_TYPE_DELETE, 'automaticdatadeletionapproval', false, 0, api::DATAREQUEST_STATUS_AWAITING_APPROVAL, 0 ], 'Delete request by user, automatic approval on' => [ false, api::DATAREQUEST_TYPE_DELETE, 'automaticdatadeletionapproval', true, 0, api::DATAREQUEST_STATUS_APPROVED, 1 ], 'Delete request by PO, automatic approval off' => [ true, api::DATAREQUEST_TYPE_DELETE, 'automaticdatadeletionapproval', false, 0, api::DATAREQUEST_STATUS_AWAITING_APPROVAL, 0 ], 'Delete request by PO, automatic approval on' => [ true, api::DATAREQUEST_TYPE_DELETE, 'automaticdatadeletionapproval', true, 'dpo', api::DATAREQUEST_STATUS_APPROVED, 1 ], ]; } /** * Test for api::create_data_request() * * @dataProvider data_request_creation_provider * @param bool $asprivacyofficer Whether the request is made as the Privacy Officer or the user itself. * @param string $type The data request type. * @param string $setting The automatic approval setting. * @param bool $automaticapproval Whether automatic data request approval is turned on or not. * @param int|string $expecteddpoval The expected value for the 'dpo' field. 'dpo' means we'd the expected value would be the * user ID of the privacy officer which happens in the case where a PO requests on behalf of * someone else and automatic data request approval is turned on. * @param int $expectedstatus The expected status of the data request. * @param int $expectedtaskcount The number of expected queued data requests tasks. * @throws coding_exception * @throws invalid_persistent_exception */ public function test_create_data_request($asprivacyofficer, $type, $setting, $automaticapproval, $expecteddpoval, $expectedstatus, $expectedtaskcount) { global $USER; $this->resetAfterTest(); $generator = new testing_data_generator(); $user = $generator->create_user(); $comment = 'sample comment'; // Login. if ($asprivacyofficer) { $this->setAdminUser(); } else { $this->setUser($user->id); } // Set the automatic data request approval setting value. set_config($setting, $automaticapproval, 'tool_dataprivacy'); // If set to 'dpo' use the currently logged-in user's ID (which should be the admin user's ID). if ($expecteddpoval === 'dpo') { $expecteddpoval = $USER->id; } // Test data request creation. $datarequest = api::create_data_request($user->id, $type, $comment); $this->assertEquals($user->id, $datarequest->get('userid')); $this->assertEquals($USER->id, $datarequest->get('requestedby')); $this->assertEquals($expecteddpoval, $datarequest->get('dpo')); $this->assertEquals($type, $datarequest->get('type')); $this->assertEquals($expectedstatus, $datarequest->get('status')); $this->assertEquals($comment, $datarequest->get('comments')); $this->assertEquals($automaticapproval, $datarequest->get('systemapproved')); // Test number of queued data request tasks. $datarequesttasks = manager::get_adhoc_tasks(process_data_request_task::class); $this->assertCount($expectedtaskcount, $datarequesttasks); } /** * Test for api::create_data_request() made by a parent. */ public function test_create_data_request_by_parent() { global $DB; $this->resetAfterTest(); $generator = new testing_data_generator(); $user = $generator->create_user(); $parent = $generator->create_user(); $comment = 'sample comment'; // Get the teacher role pretend it's the parent roles ;). $systemcontext = \context_system::instance(); $usercontext = \context_user::instance($user->id); $parentroleid = $DB->get_field('role', 'id', array('shortname' => 'teacher')); // Give the manager role with the capability to manage data requests. assign_capability('tool/dataprivacy:makedatarequestsforchildren', CAP_ALLOW, $parentroleid, $systemcontext->id, true); // Assign the parent to user. role_assign($parentroleid, $parent->id, $usercontext->id); // Login as the user's parent. $this->setUser($parent); // Test data request creation. $datarequest = api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT, $comment); $this->assertEquals($user->id, $datarequest->get('userid')); $this->assertEquals($parent->id, $datarequest->get('requestedby')); $this->assertEquals(0, $datarequest->get('dpo')); $this->assertEquals(api::DATAREQUEST_TYPE_EXPORT, $datarequest->get('type')); $this->assertEquals(api::DATAREQUEST_STATUS_AWAITING_APPROVAL, $datarequest->get('status')); $this->assertEquals($comment, $datarequest->get('comments')); } /** * Test for api::deny_data_request() */ public function test_deny_data_request() { $this->resetAfterTest(); $generator = new testing_data_generator(); $user = $generator->create_user(); $comment = 'sample comment'; // Login as user. $this->setUser($user->id); // Test data request creation. $datarequest = api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT, $comment); // Login as the admin (default DPO when no one is set). $this->setAdminUser(); // Make this ready for approval. api::update_request_status($datarequest->get('id'), api::DATAREQUEST_STATUS_AWAITING_APPROVAL); // Deny the data request. $result = api::deny_data_request($datarequest->get('id')); $this->assertTrue($result); } /** * Data provider for \tool_dataprivacy_api_testcase::test_get_data_requests(). * * @return array */ public function get_data_requests_provider() { $completeonly = [api::DATAREQUEST_STATUS_COMPLETE, api::DATAREQUEST_STATUS_DOWNLOAD_READY, api::DATAREQUEST_STATUS_DELETED]; $completeandcancelled = array_merge($completeonly, [api::DATAREQUEST_STATUS_CANCELLED]); return [ // Own data requests. ['user', false, $completeonly], // Non-DPO fetching all requets. ['user', true, $completeonly], // Admin fetching all completed and cancelled requests. ['dpo', true, $completeandcancelled], // Admin fetching all completed requests. ['dpo', true, $completeonly], // Guest fetching all requests. ['guest', true, $completeonly], ]; } /** * Test for api::get_data_requests() * * @dataProvider get_data_requests_provider * @param string $usertype The type of the user logging in. * @param boolean $fetchall Whether to fetch all records. * @param int[] $statuses Status filters. */ public function test_get_data_requests($usertype, $fetchall, $statuses) { $this->resetAfterTest(); $generator = new testing_data_generator(); $user1 = $generator->create_user(); $user2 = $generator->create_user(); $user3 = $generator->create_user(); $user4 = $generator->create_user(); $user5 = $generator->create_user(); $users = [$user1, $user2, $user3, $user4, $user5]; switch ($usertype) { case 'user': $loggeduser = $user1; break; case 'dpo': $loggeduser = get_admin(); break; case 'guest': $loggeduser = guest_user(); break; } $comment = 'Data %s request comment by user %d'; $exportstring = helper::get_shortened_request_type_string(api::DATAREQUEST_TYPE_EXPORT); $deletionstring = helper::get_shortened_request_type_string(api::DATAREQUEST_TYPE_DELETE); // Make a data requests for the users. foreach ($users as $user) { $this->setUser($user); api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT, sprintf($comment, $exportstring, $user->id)); api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT, sprintf($comment, $deletionstring, $user->id)); } // Log in as the target user. $this->setUser($loggeduser); // Get records count based on the filters. $userid = $loggeduser->id; if ($fetchall) { $userid = 0; } $count = api::get_data_requests_count($userid); if (api::is_site_dpo($loggeduser->id)) { // DPOs should see all the requests. $this->assertEquals(count($users) * 2, $count); } else { if (empty($userid)) { // There should be no data requests for this user available. $this->assertEquals(0, $count); } else { // There should be only one (request with pending status). $this->assertEquals(2, $count); } } // Get data requests. $requests = api::get_data_requests($userid); // The number of requests should match the count. $this->assertCount($count, $requests); // Test filtering by status. if ($count && !empty($statuses)) { $filteredcount = api::get_data_requests_count($userid, $statuses); // There should be none as they are all pending. $this->assertEquals(0, $filteredcount); $filteredrequests = api::get_data_requests($userid, $statuses); $this->assertCount($filteredcount, $filteredrequests); $statuscounts = []; foreach ($statuses as $stat) { $statuscounts[$stat] = 0; } $numstatus = count($statuses); // Get all requests with status filter and update statuses, randomly. foreach ($requests as $request) { if (rand(0, 1)) { continue; } if ($numstatus > 1) { $index = rand(0, $numstatus - 1); $status = $statuses[$index]; } else { $status = reset($statuses); } $statuscounts[$status]++; api::update_request_status($request->get('id'), $status); } $total = array_sum($statuscounts); $filteredcount = api::get_data_requests_count($userid, $statuses); $this->assertEquals($total, $filteredcount); $filteredrequests = api::get_data_requests($userid, $statuses); $this->assertCount($filteredcount, $filteredrequests); // Confirm the filtered requests match the status filter(s). foreach ($filteredrequests as $request) { $this->assertContainsEquals($request->get('status'), $statuses); } if ($numstatus > 1) { // Fetch by individual status to check the numbers match. foreach ($statuses as $status) { $filteredcount = api::get_data_requests_count($userid, [$status]); $this->assertEquals($statuscounts[$status], $filteredcount); $filteredrequests = api::get_data_requests($userid, [$status]); $this->assertCount($filteredcount, $filteredrequests); } } } } /** * Data provider for test_has_ongoing_request. */ public function status_provider() { return [ [api::DATAREQUEST_STATUS_AWAITING_APPROVAL, true], [api::DATAREQUEST_STATUS_APPROVED, true], [api::DATAREQUEST_STATUS_PROCESSING, true], [api::DATAREQUEST_STATUS_COMPLETE, false], [api::DATAREQUEST_STATUS_CANCELLED, false], [api::DATAREQUEST_STATUS_REJECTED, false], [api::DATAREQUEST_STATUS_DOWNLOAD_READY, false], [api::DATAREQUEST_STATUS_EXPIRED, false], [api::DATAREQUEST_STATUS_DELETED, false], ]; } /** * Test for api::has_ongoing_request() * * @dataProvider status_provider * @param int $status The request status. * @param bool $expected The expected result. */ public function test_has_ongoing_request($status, $expected) { $this->resetAfterTest(); $generator = new testing_data_generator(); $user1 = $generator->create_user(); // Make a data request as user 1. $this->setUser($user1); $request = api::create_data_request($user1->id, api::DATAREQUEST_TYPE_EXPORT); // Set the status. api::update_request_status($request->get('id'), $status); // Check if this request is ongoing. $result = api::has_ongoing_request($user1->id, api::DATAREQUEST_TYPE_EXPORT); $this->assertEquals($expected, $result); } /** * Test for api::is_active() * * @dataProvider status_provider * @param int $status The request status * @param bool $expected The expected result */ public function test_is_active($status, $expected) { // Check if this request is ongoing. $result = api::is_active($status); $this->assertEquals($expected, $result); } /** * Test for api::is_site_dpo() */ public function test_is_site_dpo() { global $DB; $this->resetAfterTest(); // No configured site DPOs yet. $admin = get_admin(); $this->assertTrue(api::is_site_dpo($admin->id)); $generator = new testing_data_generator(); $dpo = $generator->create_user(); $nondpo = $generator->create_user(); $context = \context_system::instance(); // Manager role. $managerroleid = $DB->get_field('role', 'id', array('shortname' => 'manager')); // Give the manager role with the capability to manage data requests. assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $managerroleid, $context->id, true); // Assign u1 as a manager. role_assign($managerroleid, $dpo->id, $context->id); // Map only the manager role to the DPO role. set_config('dporoles', $managerroleid, 'tool_dataprivacy'); // User is a DPO. $this->assertTrue(api::is_site_dpo($dpo->id)); // User is not a DPO. $this->assertFalse(api::is_site_dpo($nondpo->id)); } /** * Data provider function for test_notify_dpo * * @return array */ public function notify_dpo_provider() { return [ [false, api::DATAREQUEST_TYPE_EXPORT, 'requesttypeexport', 'Export my user data'], [false, api::DATAREQUEST_TYPE_DELETE, 'requesttypedelete', 'Delete my user data'], [false, api::DATAREQUEST_TYPE_OTHERS, 'requesttypeothers', 'Nothing. Just wanna say hi'], [true, api::DATAREQUEST_TYPE_EXPORT, 'requesttypeexport', 'Admin export data of another user'], ]; } /** * Test for api::notify_dpo() * * @dataProvider notify_dpo_provider * @param bool $byadmin Whether the admin requests data on behalf of the user * @param int $type The request type * @param string $typestringid The request lang string identifier * @param string $comments The requestor's message to the DPO. */ public function test_notify_dpo($byadmin, $type, $typestringid, $comments) { $this->resetAfterTest(); $generator = new testing_data_generator(); $user1 = $generator->create_user(); // Let's just use admin as DPO (It's the default if not set). $dpo = get_admin(); if ($byadmin) { $this->setAdminUser(); $requestedby = $dpo; } else { $this->setUser($user1); $requestedby = $user1; } // Make a data request for user 1. $request = api::create_data_request($user1->id, $type, $comments); $sink = $this->redirectMessages(); $messageid = api::notify_dpo($dpo, $request); $this->assertNotFalse($messageid); $messages = $sink->get_messages(); $this->assertCount(1, $messages); $message = reset($messages); // Check some of the message properties. $this->assertEquals($requestedby->id, $message->useridfrom); $this->assertEquals($dpo->id, $message->useridto); $typestring = get_string($typestringid, 'tool_dataprivacy'); $subject = get_string('datarequestemailsubject', 'tool_dataprivacy', $typestring); $this->assertEquals($subject, $message->subject); $this->assertEquals('tool_dataprivacy', $message->component); $this->assertEquals('contactdataprotectionofficer', $message->eventtype); $this->assertStringContainsString(fullname($dpo), $message->fullmessage); $this->assertStringContainsString(fullname($user1), $message->fullmessage); } /** * Test data purposes CRUD actions. * * @return null */ public function test_purpose_crud() { $this->resetAfterTest(); $this->setAdminUser(); // Add. $purpose = api::create_purpose((object)[ 'name' => 'bbb', 'description' => '<b>yeah</b>', 'descriptionformat' => 1, 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a,gdpr_art_6_1_c,gdpr_art_6_1_e' ]); $this->assertInstanceOf('\tool_dataprivacy\purpose', $purpose); $this->assertEquals('bbb', $purpose->get('name')); $this->assertEquals('PT1M', $purpose->get('retentionperiod')); $this->assertEquals('gdpr_art_6_1_a,gdpr_art_6_1_c,gdpr_art_6_1_e', $purpose->get('lawfulbases')); // Update. $purpose->set('retentionperiod', 'PT2M'); $purpose = api::update_purpose($purpose->to_record()); $this->assertEquals('PT2M', $purpose->get('retentionperiod')); // Retrieve. $purpose = api::create_purpose((object)['name' => 'aaa', 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a']); $purposes = api::get_purposes(); $this->assertCount(2, $purposes); $this->assertEquals('aaa', $purposes[0]->get('name')); $this->assertEquals('bbb', $purposes[1]->get('name')); // Delete. api::delete_purpose($purposes[0]->get('id')); $this->assertCount(1, api::get_purposes()); api::delete_purpose($purposes[1]->get('id')); $this->assertCount(0, api::get_purposes()); } /** * Test data categories CRUD actions. * * @return null */ public function test_category_crud() { $this->resetAfterTest(); $this->setAdminUser(); // Add. $category = api::create_category((object)[ 'name' => 'bbb', 'description' => '<b>yeah</b>', 'descriptionformat' => 1 ]); $this->assertInstanceOf('\tool_dataprivacy\category', $category); $this->assertEquals('bbb', $category->get('name')); // Update. $category->set('name', 'bcd'); $category = api::update_category($category->to_record()); $this->assertEquals('bcd', $category->get('name')); // Retrieve. $category = api::create_category((object)['name' => 'aaa']); $categories = api::get_categories(); $this->assertCount(2, $categories); $this->assertEquals('aaa', $categories[0]->get('name')); $this->assertEquals('bcd', $categories[1]->get('name')); // Delete. api::delete_category($categories[0]->get('id')); $this->assertCount(1, api::get_categories()); api::delete_category($categories[1]->get('id')); $this->assertCount(0, api::get_categories()); } /** * Test context instances. * * @return null */ public function test_context_instances() { global $DB; $this->resetAfterTest(); $this->setAdminUser(); list($purposes, $categories, $courses, $modules) = $this->add_purposes_and_categories(); $coursecontext1 = \context_course::instance($courses[0]->id); $coursecontext2 = \context_course::instance($courses[1]->id); $record1 = (object)['contextid' => $coursecontext1->id, 'purposeid' => $purposes[0]->get('id'), 'categoryid' => $categories[0]->get('id')]; $contextinstance1 = api::set_context_instance($record1); $record2 = (object)['contextid' => $coursecontext2->id, 'purposeid' => $purposes[1]->get('id'), 'categoryid' => $categories[1]->get('id')]; $contextinstance2 = api::set_context_instance($record2); $this->assertCount(2, $DB->get_records('tool_dataprivacy_ctxinstance')); api::unset_context_instance($contextinstance1); $this->assertCount(1, $DB->get_records('tool_dataprivacy_ctxinstance')); $update = (object)['id' => $contextinstance2->get('id'), 'contextid' => $coursecontext2->id, 'purposeid' => $purposes[0]->get('id'), 'categoryid' => $categories[0]->get('id')]; $contextinstance2 = api::set_context_instance($update); $this->assertCount(1, $DB->get_records('tool_dataprivacy_ctxinstance')); } /** * Test contextlevel. * * @return null */ public function test_contextlevel() { global $DB; $this->resetAfterTest(); $this->setAdminUser(); list($purposes, $categories, $courses, $modules) = $this->add_purposes_and_categories(); $record = (object)[ 'purposeid' => $purposes[0]->get('id'), 'categoryid' => $categories[0]->get('id'), 'contextlevel' => CONTEXT_SYSTEM, ]; $contextlevel = api::set_contextlevel($record); $this->assertInstanceOf('\tool_dataprivacy\contextlevel', $contextlevel); $this->assertEquals($record->contextlevel, $contextlevel->get('contextlevel')); $this->assertEquals($record->purposeid, $contextlevel->get('purposeid')); $this->assertEquals($record->categoryid, $contextlevel->get('categoryid')); // Now update it. $record->purposeid = $purposes[1]->get('id'); $contextlevel = api::set_contextlevel($record); $this->assertEquals($record->contextlevel, $contextlevel->get('contextlevel')); $this->assertEquals($record->purposeid, $contextlevel->get('purposeid')); $this->assertEquals(1, $DB->count_records('tool_dataprivacy_ctxlevel')); $record->contextlevel = CONTEXT_USER; $contextlevel = api::set_contextlevel($record); $this->assertEquals(2, $DB->count_records('tool_dataprivacy_ctxlevel')); } /** * Test effective context levels purpose and category defaults. * * @return null */ public function test_effective_contextlevel_defaults() { $this->setAdminUser(); $this->resetAfterTest(); list($purposes, $categories, $courses, $modules) = $this->add_purposes_and_categories(); list($purposeid, $categoryid) = data_registry::get_effective_default_contextlevel_purpose_and_category(CONTEXT_SYSTEM); $this->assertEquals(false, $purposeid); $this->assertEquals(false, $categoryid); list($purposevar, $categoryvar) = data_registry::var_names_from_context( \context_helper::get_class_for_level(CONTEXT_SYSTEM) ); set_config($purposevar, $purposes[0]->get('id'), 'tool_dataprivacy'); list($purposeid, $categoryid) = data_registry::get_effective_default_contextlevel_purpose_and_category(CONTEXT_SYSTEM); $this->assertEquals($purposes[0]->get('id'), $purposeid); $this->assertEquals(false, $categoryid); // Course defined values should have preference. list($purposevar, $categoryvar) = data_registry::var_names_from_context( \context_helper::get_class_for_level(CONTEXT_COURSE) ); set_config($purposevar, $purposes[1]->get('id'), 'tool_dataprivacy'); set_config($categoryvar, $categories[0]->get('id'), 'tool_dataprivacy'); list($purposeid, $categoryid) = data_registry::get_effective_default_contextlevel_purpose_and_category(CONTEXT_COURSE); $this->assertEquals($purposes[1]->get('id'), $purposeid); $this->assertEquals($categories[0]->get('id'), $categoryid); // Context level defaults are also allowed to be set to 'inherit'. set_config($purposevar, context_instance::INHERIT, 'tool_dataprivacy'); } /** * Ensure that when nothing is configured, all values return false. */ public function test_get_effective_contextlevel_unset() { // Before setup, get_effective_contextlevel_purpose will return false. $this->assertFalse(api::get_effective_contextlevel_category(CONTEXT_SYSTEM)); $this->assertFalse(api::get_effective_contextlevel_purpose(CONTEXT_SYSTEM)); $this->assertFalse(api::get_effective_contextlevel_category(CONTEXT_USER)); $this->assertFalse(api::get_effective_contextlevel_purpose(CONTEXT_USER)); } /** * Ensure that when nothing is configured, all values return false. */ public function test_get_effective_context_unset() { // Before setup, get_effective_contextlevel_purpose will return false. $this->assertFalse(api::get_effective_context_category(\context_system::instance())); $this->assertFalse(api::get_effective_context_purpose(\context_system::instance())); } /** * Ensure that fetching the effective value for context levels is only available to system, and user context levels. * * @dataProvider invalid_effective_contextlevel_provider * @param int $contextlevel */ public function test_set_contextlevel_invalid_contextlevels($contextlevel) { $this->expectException(\coding_exception::class); api::set_contextlevel((object) [ 'contextlevel' => $contextlevel, ]); } /** * Test effective contextlevel return. */ public function test_effective_contextlevel() { $this->resetAfterTest(); // Set the initial purpose and category. $purpose1 = api::create_purpose((object)['name' => 'p1', 'retentionperiod' => 'PT1H', 'lawfulbases' => 'gdpr_art_6_1_a']); $category1 = api::create_category((object)['name' => 'a']); api::set_contextlevel((object)[ 'contextlevel' => CONTEXT_SYSTEM, 'purposeid' => $purpose1->get('id'), 'categoryid' => $category1->get('id'), ]); $this->assertEquals($purpose1, api::get_effective_contextlevel_purpose(CONTEXT_SYSTEM)); $this->assertEquals($category1, api::get_effective_contextlevel_category(CONTEXT_SYSTEM)); // The user context inherits from the system context when not set. $this->assertEquals($purpose1, api::get_effective_contextlevel_purpose(CONTEXT_USER)); $this->assertEquals($category1, api::get_effective_contextlevel_category(CONTEXT_USER)); // Forcing the behaviour to inherit will have the same result. api::set_contextlevel((object) [ 'contextlevel' => CONTEXT_USER, 'purposeid' => context_instance::INHERIT, 'categoryid' => context_instance::INHERIT, ]); $this->assertEquals($purpose1, api::get_effective_contextlevel_purpose(CONTEXT_USER)); $this->assertEquals($category1, api::get_effective_contextlevel_category(CONTEXT_USER)); // Setting specific values will override the inheritance behaviour. $purpose2 = api::create_purpose((object)['name' => 'p2', 'retentionperiod' => 'PT2H', 'lawfulbases' => 'gdpr_art_6_1_a']); $category2 = api::create_category((object)['name' => 'b']); // Set the system context level to purpose 1. api::set_contextlevel((object) [ 'contextlevel' => CONTEXT_USER, 'purposeid' => $purpose2->get('id'), 'categoryid' => $category2->get('id'), ]); $this->assertEquals($purpose2, api::get_effective_contextlevel_purpose(CONTEXT_USER)); $this->assertEquals($category2, api::get_effective_contextlevel_category(CONTEXT_USER)); } /** * Ensure that fetching the effective value for context levels is only available to system, and user context levels. * * @dataProvider invalid_effective_contextlevel_provider * @param int $contextlevel */ public function test_effective_contextlevel_invalid_contextlevels($contextlevel) { $this->resetAfterTest(); $purpose1 = api::create_purpose((object)['name' => 'p1', 'retentionperiod' => 'PT1H', 'lawfulbases' => 'gdpr_art_6_1_a']); $category1 = api::create_category((object)['name' => 'a']); api::set_contextlevel((object)[ 'contextlevel' => CONTEXT_SYSTEM, 'purposeid' => $purpose1->get('id'), 'categoryid' => $category1->get('id'), ]); $this->expectException(\coding_exception::class); api::get_effective_contextlevel_purpose($contextlevel); } /** * Data provider for invalid contextlevel fetchers. */ public function invalid_effective_contextlevel_provider() { return [ [CONTEXT_COURSECAT], [CONTEXT_COURSE], [CONTEXT_MODULE], [CONTEXT_BLOCK], ]; } /** * Ensure that context inheritance works up the context tree. */ public function test_effective_context_inheritance() { $this->resetAfterTest(); $systemdata = $this->create_and_set_purpose_for_contextlevel('PT1S', CONTEXT_SYSTEM); /* * System * - Cat * - Subcat * - Course * - Forum * - User * - User block */ $cat = $this->getDataGenerator()->create_category(); $subcat = $this->getDataGenerator()->create_category(['parent' => $cat->id]); $course = $this->getDataGenerator()->create_course(['category' => $subcat->id]); $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); list(, $forumcm) = get_course_and_cm_from_instance($forum->id, 'forum'); $user = $this->getDataGenerator()->create_user(); $contextsystem = \context_system::instance(); $contextcat = \context_coursecat::instance($cat->id); $contextsubcat = \context_coursecat::instance($subcat->id); $contextcourse = \context_course::instance($course->id); $contextforum = \context_module::instance($forumcm->id); $contextuser = \context_user::instance($user->id); // Initially everything is set to Inherit. $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextsystem)); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcat)); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcat, "-1")); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcat, "0")); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextsubcat)); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextsubcat, "-1")); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextsubcat, "0")); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcourse)); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcourse, "-1")); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcourse, "0")); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextforum)); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextforum, "-1")); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextforum, "0")); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextuser)); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextuser, "-1")); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextuser, "0")); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextsystem)); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcat)); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcat, "-1")); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcat, "0")); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextsubcat)); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextsubcat, "-1")); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextsubcat, "0")); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcourse)); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcourse, "-1")); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcourse, "0")); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextforum)); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextforum, "-1")); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextforum, "0")); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextuser)); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextuser, "-1")); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextuser, "0")); // When actively set, user will use the specified value. $userdata = $this->create_and_set_purpose_for_contextlevel('PT1S', CONTEXT_USER); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextsystem)); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcat)); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcat, "-1")); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcat, "0")); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextsubcat)); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextsubcat, "-1")); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextsubcat, "0")); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcourse)); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcourse, "-1")); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcourse, "0")); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextforum)); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextforum, "-1")); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextforum, "0")); $this->assertEquals($userdata->purpose, api::get_effective_context_purpose($contextuser)); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextuser, "-1")); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextsystem)); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcat)); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcat, "-1")); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcat, "0")); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextsubcat)); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextsubcat, "-1")); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextsubcat, "0")); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcourse)); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcourse, "-1")); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcourse, "0")); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextforum)); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextforum, "-1")); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextforum, "0")); $this->assertEquals($userdata->category, api::get_effective_context_category($contextuser)); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextuser, "-1")); // Set a context for the top category. $catpurpose = new purpose(0, (object) [ 'name' => 'Purpose', 'retentionperiod' => 'P1D', 'lawfulbases' => 'gdpr_art_6_1_a', ]); $catpurpose->save(); $catcategory = new category(0, (object) ['name' => 'Category']); $catcategory->save(); api::set_context_instance((object) [ 'contextid' => $contextcat->id, 'purposeid' => $catpurpose->get('id'), 'categoryid' => $catcategory->get('id'), ]); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextsystem)); $this->assertEquals($catpurpose, api::get_effective_context_purpose($contextcat)); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcat, "-1")); $this->assertEquals($catpurpose, api::get_effective_context_purpose($contextcat, "0")); $this->assertEquals($catpurpose, api::get_effective_context_purpose($contextsubcat)); $this->assertEquals($catpurpose, api::get_effective_context_purpose($contextsubcat, "-1")); $this->assertEquals($catpurpose, api::get_effective_context_purpose($contextsubcat, "0")); $this->assertEquals($catpurpose, api::get_effective_context_purpose($contextcourse)); $this->assertEquals($catpurpose, api::get_effective_context_purpose($contextcourse, "-1")); $this->assertEquals($catpurpose, api::get_effective_context_purpose($contextcourse, "0")); $this->assertEquals($catpurpose, api::get_effective_context_purpose($contextforum)); $this->assertEquals($catpurpose, api::get_effective_context_purpose($contextforum, "-1")); $this->assertEquals($catpurpose, api::get_effective_context_purpose($contextforum, "0")); $this->assertEquals($catpurpose, api::get_effective_context_purpose($contextforum, "0")); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextsystem)); $this->assertEquals($catcategory, api::get_effective_context_category($contextcat)); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcat, "-1")); $this->assertEquals($catcategory, api::get_effective_context_category($contextcat, "0")); $this->assertEquals($catcategory, api::get_effective_context_category($contextsubcat)); $this->assertEquals($catcategory, api::get_effective_context_category($contextsubcat, "-1")); $this->assertEquals($catcategory, api::get_effective_context_category($contextsubcat, "0")); $this->assertEquals($catcategory, api::get_effective_context_category($contextcourse)); $this->assertEquals($catcategory, api::get_effective_context_category($contextcourse, "-1")); $this->assertEquals($catcategory, api::get_effective_context_category($contextcourse, "0")); $this->assertEquals($catcategory, api::get_effective_context_category($contextforum)); $this->assertEquals($catcategory, api::get_effective_context_category($contextforum, "-1")); $this->assertEquals($catcategory, api::get_effective_context_category($contextforum, "0")); // Set a context for the sub category. $subcatpurpose = new purpose(0, (object) [ 'name' => 'Purpose', 'retentionperiod' => 'P1D', 'lawfulbases' => 'gdpr_art_6_1_a', ]); $subcatpurpose->save(); $subcatcategory = new category(0, (object) ['name' => 'Category']); $subcatcategory->save(); api::set_context_instance((object) [ 'contextid' => $contextsubcat->id, 'purposeid' => $subcatpurpose->get('id'), 'categoryid' => $subcatcategory->get('id'), ]); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextsystem)); $this->assertEquals($catpurpose, api::get_effective_context_purpose($contextcat)); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcat, "-1")); $this->assertEquals($catpurpose, api::get_effective_context_purpose($contextcat, "0")); $this->assertEquals($subcatpurpose, api::get_effective_context_purpose($contextsubcat)); $this->assertEquals($catpurpose, api::get_effective_context_purpose($contextsubcat, "-1")); $this->assertEquals($subcatpurpose, api::get_effective_context_purpose($contextsubcat, "0")); $this->assertEquals($subcatpurpose, api::get_effective_context_purpose($contextcourse)); $this->assertEquals($subcatpurpose, api::get_effective_context_purpose($contextcourse, "-1")); $this->assertEquals($subcatpurpose, api::get_effective_context_purpose($contextcourse, "0")); $this->assertEquals($subcatpurpose, api::get_effective_context_purpose($contextforum)); $this->assertEquals($subcatpurpose, api::get_effective_context_purpose($contextforum, "-1")); $this->assertEquals($subcatpurpose, api::get_effective_context_purpose($contextforum, "0")); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextsystem)); $this->assertEquals($catcategory, api::get_effective_context_category($contextcat)); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcat, "-1")); $this->assertEquals($catcategory, api::get_effective_context_category($contextcat, "0")); $this->assertEquals($subcatcategory, api::get_effective_context_category($contextsubcat)); $this->assertEquals($catcategory, api::get_effective_context_category($contextsubcat, "-1")); $this->assertEquals($subcatcategory, api::get_effective_context_category($contextsubcat, "0")); $this->assertEquals($subcatcategory, api::get_effective_context_category($contextcourse)); $this->assertEquals($subcatcategory, api::get_effective_context_category($contextcourse, "-1")); $this->assertEquals($subcatcategory, api::get_effective_context_category($contextcourse, "0")); $this->assertEquals($subcatcategory, api::get_effective_context_category($contextforum)); $this->assertEquals($subcatcategory, api::get_effective_context_category($contextforum, "-1")); $this->assertEquals($subcatcategory, api::get_effective_context_category($contextforum, "0")); // Set a context for the course. $coursepurpose = new purpose(0, (object) [ 'name' => 'Purpose', 'retentionperiod' => 'P1D', 'lawfulbases' => 'gdpr_art_6_1_a', ]); $coursepurpose->save(); $coursecategory = new category(0, (object) ['name' => 'Category']); $coursecategory->save(); api::set_context_instance((object) [ 'contextid' => $contextcourse->id, 'purposeid' => $coursepurpose->get('id'), 'categoryid' => $coursecategory->get('id'), ]); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextsystem)); $this->assertEquals($catpurpose, api::get_effective_context_purpose($contextcat)); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcat, "-1")); $this->assertEquals($catpurpose, api::get_effective_context_purpose($contextcat, "0")); $this->assertEquals($subcatpurpose, api::get_effective_context_purpose($contextsubcat)); $this->assertEquals($catpurpose, api::get_effective_context_purpose($contextsubcat, "-1")); $this->assertEquals($subcatpurpose, api::get_effective_context_purpose($contextsubcat, "0")); $this->assertEquals($coursepurpose, api::get_effective_context_purpose($contextcourse)); $this->assertEquals($subcatpurpose, api::get_effective_context_purpose($contextcourse, "-1")); $this->assertEquals($coursepurpose, api::get_effective_context_purpose($contextcourse, "0")); $this->assertEquals($coursepurpose, api::get_effective_context_purpose($contextforum)); $this->assertEquals($coursepurpose, api::get_effective_context_purpose($contextforum, "-1")); $this->assertEquals($coursepurpose, api::get_effective_context_purpose($contextforum, "0")); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextsystem)); $this->assertEquals($catcategory, api::get_effective_context_category($contextcat)); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcat, "-1")); $this->assertEquals($catcategory, api::get_effective_context_category($contextcat, "0")); $this->assertEquals($subcatcategory, api::get_effective_context_category($contextsubcat)); $this->assertEquals($catcategory, api::get_effective_context_category($contextsubcat, "-1")); $this->assertEquals($subcatcategory, api::get_effective_context_category($contextsubcat, "0")); $this->assertEquals($coursecategory, api::get_effective_context_category($contextcourse)); $this->assertEquals($subcatcategory, api::get_effective_context_category($contextcourse, "-1")); $this->assertEquals($coursecategory, api::get_effective_context_category($contextcourse, "0")); $this->assertEquals($coursecategory, api::get_effective_context_category($contextforum)); $this->assertEquals($coursecategory, api::get_effective_context_category($contextforum, "-1")); $this->assertEquals($coursecategory, api::get_effective_context_category($contextforum, "0")); // Set a context for the forum. $forumpurpose = new purpose(0, (object) [ 'name' => 'Purpose', 'retentionperiod' => 'P1D', 'lawfulbases' => 'gdpr_art_6_1_a', ]); $forumpurpose->save(); $forumcategory = new category(0, (object) ['name' => 'Category']); $forumcategory->save(); api::set_context_instance((object) [ 'contextid' => $contextforum->id, 'purposeid' => $forumpurpose->get('id'), 'categoryid' => $forumcategory->get('id'), ]); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextsystem)); $this->assertEquals($catpurpose, api::get_effective_context_purpose($contextcat)); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcat, "-1")); $this->assertEquals($catpurpose, api::get_effective_context_purpose($contextcat, "0")); $this->assertEquals($subcatpurpose, api::get_effective_context_purpose($contextsubcat)); $this->assertEquals($catpurpose, api::get_effective_context_purpose($contextsubcat, "-1")); $this->assertEquals($subcatpurpose, api::get_effective_context_purpose($contextsubcat, "0")); $this->assertEquals($coursepurpose, api::get_effective_context_purpose($contextcourse)); $this->assertEquals($subcatpurpose, api::get_effective_context_purpose($contextcourse, "-1")); $this->assertEquals($coursepurpose, api::get_effective_context_purpose($contextcourse, "0")); $this->assertEquals($forumpurpose, api::get_effective_context_purpose($contextforum)); $this->assertEquals($coursepurpose, api::get_effective_context_purpose($contextforum, "-1")); $this->assertEquals($forumpurpose, api::get_effective_context_purpose($contextforum, "0")); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextsystem)); $this->assertEquals($catcategory, api::get_effective_context_category($contextcat)); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcat, "-1")); $this->assertEquals($catcategory, api::get_effective_context_category($contextcat, "0")); $this->assertEquals($subcatcategory, api::get_effective_context_category($contextsubcat)); $this->assertEquals($catcategory, api::get_effective_context_category($contextsubcat, "-1")); $this->assertEquals($subcatcategory, api::get_effective_context_category($contextsubcat, "0")); $this->assertEquals($coursecategory, api::get_effective_context_category($contextcourse)); $this->assertEquals($subcatcategory, api::get_effective_context_category($contextcourse, "-1")); $this->assertEquals($coursecategory, api::get_effective_context_category($contextcourse, "0")); $this->assertEquals($forumcategory, api::get_effective_context_category($contextforum)); $this->assertEquals($coursecategory, api::get_effective_context_category($contextforum, "-1")); $this->assertEquals($forumcategory, api::get_effective_context_category($contextforum, "0")); } /** * Ensure that context inheritance works up the context tree when inherit values are explicitly set at the * contextlevel. * * Although it should not be possible to set hard INHERIT values at this level, there may be legacy data which still * contains this. */ public function test_effective_context_inheritance_explicitly_set() { $this->resetAfterTest(); $systemdata = $this->create_and_set_purpose_for_contextlevel('PT1S', CONTEXT_SYSTEM); /* * System * - Cat * - Subcat * - Course * - Forum * - User * - User block */ $cat = $this->getDataGenerator()->create_category(); $subcat = $this->getDataGenerator()->create_category(['parent' => $cat->id]); $course = $this->getDataGenerator()->create_course(['category' => $subcat->id]); $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); list(, $forumcm) = get_course_and_cm_from_instance($forum->id, 'forum'); $contextsystem = \context_system::instance(); $contextcat = \context_coursecat::instance($cat->id); $contextsubcat = \context_coursecat::instance($subcat->id); $contextcourse = \context_course::instance($course->id); $contextforum = \context_module::instance($forumcm->id); // Initially everything is set to Inherit. $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextsystem)); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcat)); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextsubcat)); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcourse)); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextforum)); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextsystem)); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcat)); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextsubcat)); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcourse)); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextforum)); // Set a default value of inherit for CONTEXT_COURSECAT. $classname = \context_helper::get_class_for_level(CONTEXT_COURSECAT); list($purposevar, $categoryvar) = data_registry::var_names_from_context($classname); set_config($purposevar, '-1', 'tool_dataprivacy'); set_config($categoryvar, '-1', 'tool_dataprivacy'); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextsystem)); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcat)); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextsubcat)); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcourse)); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextforum)); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextsystem)); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcat)); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextsubcat)); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcourse)); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextforum)); // Set a default value of inherit for CONTEXT_COURSE. $classname = \context_helper::get_class_for_level(CONTEXT_COURSE); list($purposevar, $categoryvar) = data_registry::var_names_from_context($classname); set_config($purposevar, '-1', 'tool_dataprivacy'); set_config($categoryvar, '-1', 'tool_dataprivacy'); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextsystem)); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcat)); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextsubcat)); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcourse)); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextforum)); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextsystem)); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcat)); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextsubcat)); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcourse)); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextforum)); // Set a default value of inherit for CONTEXT_MODULE. $classname = \context_helper::get_class_for_level(CONTEXT_MODULE); list($purposevar, $categoryvar) = data_registry::var_names_from_context($classname); set_config($purposevar, '-1', 'tool_dataprivacy'); set_config($categoryvar, '-1', 'tool_dataprivacy'); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextsystem)); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcat)); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextsubcat)); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextcourse)); $this->assertEquals($systemdata->purpose, api::get_effective_context_purpose($contextforum)); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextsystem)); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcat)); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextsubcat)); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextcourse)); $this->assertEquals($systemdata->category, api::get_effective_context_category($contextforum)); } /** * Creates test purposes and categories. * * @return null */ protected function add_purposes_and_categories() { $this->resetAfterTest(); $purpose1 = api::create_purpose((object)['name' => 'p1', 'retentionperiod' => 'PT1H', 'lawfulbases' => 'gdpr_art_6_1_a']); $purpose2 = api::create_purpose((object)['name' => 'p2', 'retentionperiod' => 'PT2H', 'lawfulbases' => 'gdpr_art_6_1_b']); $purpose3 = api::create_purpose((object)['name' => 'p3', 'retentionperiod' => 'PT3H', 'lawfulbases' => 'gdpr_art_6_1_c']); $cat1 = api::create_category((object)['name' => 'a']); $cat2 = api::create_category((object)['name' => 'b']); $cat3 = api::create_category((object)['name' => 'c']); $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $module1 = $this->getDataGenerator()->create_module('resource', array('course' => $course1)); $module2 = $this->getDataGenerator()->create_module('resource', array('course' => $course2)); return [ [$purpose1, $purpose2, $purpose3], [$cat1, $cat2, $cat3], [$course1, $course2], [$module1, $module2] ]; } /** * Test that delete requests do not filter out protected purpose contexts if the the site is properly configured. */ public function test_get_approved_contextlist_collection_for_collection_delete_course_no_site_config() { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - YEARSECS]); $coursecontext = \context_course::instance($course->id); $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); list(, $forumcm) = get_course_and_cm_from_instance($forum->id, 'forum'); $contextforum = \context_module::instance($forumcm->id); $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student'); // Create the initial contextlist. $initialcollection = new \core_privacy\local\request\contextlist_collection($user->id); $contextlist = new \core_privacy\local\request\contextlist(); $contextlist->add_from_sql('SELECT id FROM {context} WHERE id = :contextid', ['contextid' => $coursecontext->id]); $contextlist->set_component('tool_dataprivacy'); $initialcollection->add_contextlist($contextlist); $contextlist = new \core_privacy\local\request\contextlist(); $contextlist->add_from_sql('SELECT id FROM {context} WHERE id = :contextid', ['contextid' => $contextforum->id]); $contextlist->set_component('mod_forum'); $initialcollection->add_contextlist($contextlist); $collection = api::get_approved_contextlist_collection_for_collection( $initialcollection, $user, api::DATAREQUEST_TYPE_DELETE); $this->assertCount(2, $collection); $list = $collection->get_contextlist_for_component('tool_dataprivacy'); $this->assertCount(1, $list); $list = $collection->get_contextlist_for_component('mod_forum'); $this->assertCount(1, $list); } /** * Test that delete requests do not filter out protected purpose contexts if they are already expired. */ public function test_get_approved_contextlist_collection_for_collection_delete_course_expired_protected() { $this->resetAfterTest(); $purposes = $this->setup_basics('PT1H', 'PT1H', 'PT1H'); $purposes->course->purpose->set('protected', 1)->save(); $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - YEARSECS]); $coursecontext = \context_course::instance($course->id); $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student'); // Create the initial contextlist. $contextlist = new \core_privacy\local\request\contextlist(); $contextlist->add_from_sql('SELECT id FROM {context} WHERE id = :contextid', ['contextid' => $coursecontext->id]); $contextlist->set_component('tool_dataprivacy'); $initialcollection = new \core_privacy\local\request\contextlist_collection($user->id); $initialcollection->add_contextlist($contextlist); $purposes->course->purpose->set('protected', 1)->save(); $collection = api::get_approved_contextlist_collection_for_collection( $initialcollection, $user, api::DATAREQUEST_TYPE_DELETE); $this->assertCount(1, $collection); $list = $collection->get_contextlist_for_component('tool_dataprivacy'); $this->assertCount(1, $list); } /** * Test that delete requests does filter out protected purpose contexts which are not expired. */ public function test_get_approved_contextlist_collection_for_collection_delete_course_unexpired_protected() { $this->resetAfterTest(); $purposes = $this->setup_basics('PT1H', 'PT1H', 'P1Y'); $purposes->course->purpose->set('protected', 1)->save(); $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time()]); $coursecontext = \context_course::instance($course->id); $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student'); // Create the initial contextlist. $contextlist = new \core_privacy\local\request\contextlist(); $contextlist->add_from_sql('SELECT id FROM {context} WHERE id = :contextid', ['contextid' => $coursecontext->id]); $contextlist->set_component('tool_dataprivacy'); $initialcollection = new \core_privacy\local\request\contextlist_collection($user->id); $initialcollection->add_contextlist($contextlist); $purposes->course->purpose->set('protected', 1)->save(); $collection = api::get_approved_contextlist_collection_for_collection( $initialcollection, $user, api::DATAREQUEST_TYPE_DELETE); $this->assertCount(0, $collection); $list = $collection->get_contextlist_for_component('tool_dataprivacy'); $this->assertEmpty($list); } /** * Test that delete requests do not filter out unexpired contexts if they are not protected. */ public function test_get_approved_contextlist_collection_for_collection_delete_course_unexpired_unprotected() { $this->resetAfterTest(); $purposes = $this->setup_basics('PT1H', 'PT1H', 'P1Y'); $purposes->course->purpose->set('protected', 1)->save(); $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time()]); $coursecontext = \context_course::instance($course->id); $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student'); // Create the initial contextlist. $contextlist = new \core_privacy\local\request\contextlist(); $contextlist->add_from_sql('SELECT id FROM {context} WHERE id = :contextid', ['contextid' => $coursecontext->id]); $contextlist->set_component('tool_dataprivacy'); $initialcollection = new \core_privacy\local\request\contextlist_collection($user->id); $initialcollection->add_contextlist($contextlist); $purposes->course->purpose->set('protected', 0)->save(); $collection = api::get_approved_contextlist_collection_for_collection( $initialcollection, $user, api::DATAREQUEST_TYPE_DELETE); $this->assertCount(1, $collection); $list = $collection->get_contextlist_for_component('tool_dataprivacy'); $this->assertCount(1, $list); } /** * Data provider for \tool_dataprivacy_api_testcase::test_set_context_defaults */ public function set_context_defaults_provider() { $contextlevels = [ [CONTEXT_COURSECAT], [CONTEXT_COURSE], [CONTEXT_MODULE], [CONTEXT_BLOCK], ]; $paramsets = [ [true, true, false, false], // Inherit category and purpose, Not for activity, Don't override. [true, false, false, false], // Inherit category but not purpose, Not for activity, Don't override. [false, true, false, false], // Inherit purpose but not category, Not for activity, Don't override. [false, false, false, false], // Don't inherit both category and purpose, Not for activity, Don't override. [false, false, false, true], // Don't inherit both category and purpose, Not for activity, Override instances. ]; $data = []; foreach ($contextlevels as $level) { foreach ($paramsets as $set) { $data[] = array_merge($level, $set); } if ($level == CONTEXT_MODULE) { // Add a combination where defaults for activity is being set. $data[] = [CONTEXT_MODULE, false, false, true, false]; $data[] = [CONTEXT_MODULE, false, false, true, true]; } } return $data; } /** * Test for \tool_dataprivacy\api::set_context_defaults() * * @dataProvider set_context_defaults_provider * @param int $contextlevel The context level * @param bool $inheritcategory Whether to set category value as INHERIT. * @param bool $inheritpurpose Whether to set purpose value as INHERIT. * @param bool $foractivity Whether to set defaults for an activity. * @param bool $override Whether to override instances. */ public function test_set_context_defaults($contextlevel, $inheritcategory, $inheritpurpose, $foractivity, $override) { $this->resetAfterTest(); $generator = $this->getDataGenerator(); // Generate course cat, course, block, assignment, forum instances. $coursecat = $generator->create_category(); $course = $generator->create_course(['category' => $coursecat->id]); $block = $generator->create_block('online_users'); $assign = $generator->create_module('assign', ['course' => $course->id]); $forum = $generator->create_module('forum', ['course' => $course->id]); $coursecatcontext = \context_coursecat::instance($coursecat->id); $coursecontext = \context_course::instance($course->id); $blockcontext = \context_block::instance($block->id); list($course, $assigncm) = get_course_and_cm_from_instance($assign->id, 'assign'); list($course, $forumcm) = get_course_and_cm_from_instance($forum->id, 'forum'); $assigncontext = \context_module::instance($assigncm->id); $forumcontext = \context_module::instance($forumcm->id); // Generate purposes and categories. $category1 = api::create_category((object)['name' => 'Test category 1']); $category2 = api::create_category((object)['name' => 'Test category 2']); $purpose1 = api::create_purpose((object)[ 'name' => 'Test purpose 1', 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a' ]); $purpose2 = api::create_purpose((object)[ 'name' => 'Test purpose 2', 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a' ]); // Assign purposes and categories to contexts. $coursecatctxinstance = api::set_context_instance((object) [ 'contextid' => $coursecatcontext->id, 'purposeid' => $purpose1->get('id'), 'categoryid' => $category1->get('id'), ]); $coursectxinstance = api::set_context_instance((object) [ 'contextid' => $coursecontext->id, 'purposeid' => $purpose1->get('id'), 'categoryid' => $category1->get('id'), ]); $blockctxinstance = api::set_context_instance((object) [ 'contextid' => $blockcontext->id, 'purposeid' => $purpose1->get('id'), 'categoryid' => $category1->get('id'), ]); $assignctxinstance = api::set_context_instance((object) [ 'contextid' => $assigncontext->id, 'purposeid' => $purpose1->get('id'), 'categoryid' => $category1->get('id'), ]); $forumctxinstance = api::set_context_instance((object) [ 'contextid' => $forumcontext->id, 'purposeid' => $purpose1->get('id'), 'categoryid' => $category1->get('id'), ]); $categoryid = $inheritcategory ? context_instance::INHERIT : $category2->get('id'); $purposeid = $inheritpurpose ? context_instance::INHERIT : $purpose2->get('id'); $activity = ''; if ($contextlevel == CONTEXT_MODULE && $foractivity) { $activity = 'assign'; } $result = api::set_context_defaults($contextlevel, $categoryid, $purposeid, $activity, $override); $this->assertTrue($result); $targetctxinstance = false; switch ($contextlevel) { case CONTEXT_COURSECAT: $targetctxinstance = $coursecatctxinstance; break; case CONTEXT_COURSE: $targetctxinstance = $coursectxinstance; break; case CONTEXT_MODULE: $targetctxinstance = $assignctxinstance; break; case CONTEXT_BLOCK: $targetctxinstance = $blockctxinstance; break; } $this->assertNotFalse($targetctxinstance); // Check the context instances. $instanceexists = context_instance::record_exists($targetctxinstance->get('id')); if ($override) { // If overridden, context instances on this context level would have been deleted. $this->assertFalse($instanceexists); // Check forum context instance. $forumctxexists = context_instance::record_exists($forumctxinstance->get('id')); if ($contextlevel != CONTEXT_MODULE || $foractivity) { // The forum context instance won't be affected in this test if: // - The overridden defaults are not for context modules. // - Only the defaults for assign have been set. $this->assertTrue($forumctxexists); } else { // If we're overriding for the whole course module context level, // then this forum context instance will be deleted as well. $this->assertFalse($forumctxexists); } } else { // Otherwise, the context instance record remains. $this->assertTrue($instanceexists); } // Check defaults. list($defaultpurpose, $defaultcategory) = data_registry::get_defaults($contextlevel, $activity); if (!$inheritpurpose) { $this->assertEquals($purposeid, $defaultpurpose); } if (!$inheritcategory) { $this->assertEquals($categoryid, $defaultcategory); } } /** * Setup the basics with the specified retention period. * * @param string $system Retention policy for the system. * @param string $user Retention policy for users. * @param string $course Retention policy for courses. * @param string $activity Retention policy for activities. */ protected function setup_basics(string $system, string $user, string $course = null, string $activity = null) : \stdClass { $this->resetAfterTest(); $purposes = (object) [ 'system' => $this->create_and_set_purpose_for_contextlevel($system, CONTEXT_SYSTEM), 'user' => $this->create_and_set_purpose_for_contextlevel($user, CONTEXT_USER), ]; if (null !== $course) { $purposes->course = $this->create_and_set_purpose_for_contextlevel($course, CONTEXT_COURSE); } if (null !== $activity) { $purposes->activity = $this->create_and_set_purpose_for_contextlevel($activity, CONTEXT_MODULE); } return $purposes; } /** * Create a retention period and set it for the specified context level. * * @param string $retention * @param int $contextlevel */ protected function create_and_set_purpose_for_contextlevel(string $retention, int $contextlevel) { $purpose = new purpose(0, (object) [ 'name' => 'Test purpose ' . rand(1, 1000), 'retentionperiod' => $retention, 'lawfulbases' => 'gdpr_art_6_1_a', ]); $purpose->create(); $cat = new category(0, (object) ['name' => 'Test category']); $cat->create(); if ($contextlevel <= CONTEXT_USER) { $record = (object) [ 'purposeid' => $purpose->get('id'), 'categoryid' => $cat->get('id'), 'contextlevel' => $contextlevel, ]; api::set_contextlevel($record); } else { list($purposevar, ) = data_registry::var_names_from_context( \context_helper::get_class_for_level(CONTEXT_COURSE) ); set_config($purposevar, $purpose->get('id'), 'tool_dataprivacy'); } return (object) [ 'purpose' => $purpose, 'category' => $cat, ]; } /** * Ensure that the find_ongoing_request_types_for_users only returns requests which are active. */ public function test_find_ongoing_request_types_for_users() { $this->resetAfterTest(); // Create users and their requests:. // - u1 has no requests of any type. // - u2 has one rejected export request. // - u3 has one rejected other request. // - u4 has one rejected delete request. // - u5 has one active and one rejected export request. // - u6 has one active and one rejected other request. // - u7 has one active and one rejected delete request. // - u8 has one active export, and one active delete request. $u1 = $this->getDataGenerator()->create_user(); $u1expect = (object) []; $u2 = $this->getDataGenerator()->create_user(); $this->create_request_with_type_and_status($u2->id, api::DATAREQUEST_TYPE_EXPORT, api::DATAREQUEST_STATUS_REJECTED); $u2expect = (object) []; $u3 = $this->getDataGenerator()->create_user(); $this->create_request_with_type_and_status($u3->id, api::DATAREQUEST_TYPE_OTHERS, api::DATAREQUEST_STATUS_REJECTED); $u3expect = (object) []; $u4 = $this->getDataGenerator()->create_user(); $this->create_request_with_type_and_status($u4->id, api::DATAREQUEST_TYPE_DELETE, api::DATAREQUEST_STATUS_REJECTED); $u4expect = (object) []; $u5 = $this->getDataGenerator()->create_user(); $this->create_request_with_type_and_status($u5->id, api::DATAREQUEST_TYPE_EXPORT, api::DATAREQUEST_STATUS_REJECTED); $this->create_request_with_type_and_status($u5->id, api::DATAREQUEST_TYPE_EXPORT, api::DATAREQUEST_STATUS_APPROVED); $u5expect = (object) [ api::DATAREQUEST_TYPE_EXPORT => true, ]; $u6 = $this->getDataGenerator()->create_user(); $this->create_request_with_type_and_status($u6->id, api::DATAREQUEST_TYPE_OTHERS, api::DATAREQUEST_STATUS_REJECTED); $this->create_request_with_type_and_status($u6->id, api::DATAREQUEST_TYPE_OTHERS, api::DATAREQUEST_STATUS_APPROVED); $u6expect = (object) [ api::DATAREQUEST_TYPE_OTHERS => true, ]; $u7 = $this->getDataGenerator()->create_user(); $this->create_request_with_type_and_status($u7->id, api::DATAREQUEST_TYPE_DELETE, api::DATAREQUEST_STATUS_REJECTED); $this->create_request_with_type_and_status($u7->id, api::DATAREQUEST_TYPE_DELETE, api::DATAREQUEST_STATUS_APPROVED); $u7expect = (object) [ api::DATAREQUEST_TYPE_DELETE => true, ]; $u8 = $this->getDataGenerator()->create_user(); $this->create_request_with_type_and_status($u8->id, api::DATAREQUEST_TYPE_EXPORT, api::DATAREQUEST_STATUS_APPROVED); $this->create_request_with_type_and_status($u8->id, api::DATAREQUEST_TYPE_DELETE, api::DATAREQUEST_STATUS_APPROVED); $u8expect = (object) [ api::DATAREQUEST_TYPE_EXPORT => true, api::DATAREQUEST_TYPE_DELETE => true, ]; // Test with no users specified. $result = api::find_ongoing_request_types_for_users([]); $this->assertEquals([], $result); // Fetch a subset of the users. $result = api::find_ongoing_request_types_for_users([$u3->id, $u4->id, $u5->id]); $this->assertEquals([ $u3->id => $u3expect, $u4->id => $u4expect, $u5->id => $u5expect, ], $result); // Fetch the empty user. $result = api::find_ongoing_request_types_for_users([$u1->id]); $this->assertEquals([ $u1->id => $u1expect, ], $result); // Fetch all. $result = api::find_ongoing_request_types_for_users( [$u1->id, $u2->id, $u3->id, $u4->id, $u5->id, $u6->id, $u7->id, $u8->id]); $this->assertEquals([ $u1->id => $u1expect, $u2->id => $u2expect, $u3->id => $u3expect, $u4->id => $u4expect, $u5->id => $u5expect, $u6->id => $u6expect, $u7->id => $u7expect, $u8->id => $u8expect, ], $result); } /** * Create a new data request for the user with the type and status specified. * * @param int $userid * @param int $type * @param int $status * @return \tool_dataprivacy\data_request */ protected function create_request_with_type_and_status(int $userid, int $type, int $status) : \tool_dataprivacy\data_request { $request = new \tool_dataprivacy\data_request(0, (object) [ 'userid' => $userid, 'type' => $type, 'status' => $status, ]); $request->save(); return $request; } /** * Test whether user can create data download request for themselves */ public function test_can_create_data_download_request_for_self(): void { global $DB; $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); // The default user role allows for the creation of download data requests. $this->assertTrue(api::can_create_data_download_request_for_self()); // Prohibit that capability. $userrole = $DB->get_field('role', 'id', ['shortname' => 'user'], MUST_EXIST); assign_capability('tool/dataprivacy:downloadownrequest', CAP_PROHIBIT, $userrole, \context_user::instance($user->id)); $this->assertFalse(api::can_create_data_download_request_for_self()); } /** * Test user cannot create data deletion request for themselves if they don't have * "tool/dataprivacy:requestdelete" capability. * * @throws coding_exception */ public function test_can_create_data_deletion_request_for_self_no() { $this->resetAfterTest(); $userid = $this->getDataGenerator()->create_user()->id; $roleid = $this->getDataGenerator()->create_role(); assign_capability('tool/dataprivacy:requestdelete', CAP_PROHIBIT, $roleid, \context_user::instance($userid)); role_assign($roleid, $userid, \context_user::instance($userid)); $this->setUser($userid); $this->assertFalse(api::can_create_data_deletion_request_for_self()); } /** * Test primary admin cannot create data deletion request for themselves */ public function test_can_create_data_deletion_request_for_self_primary_admin() { $this->resetAfterTest(); $this->setAdminUser(); $this->assertFalse(api::can_create_data_deletion_request_for_self()); } /** * Test secondary admin can create data deletion request for themselves */ public function test_can_create_data_deletion_request_for_self_secondary_admin() { $this->resetAfterTest(); $admin1 = $this->getDataGenerator()->create_user(); $admin2 = $this->getDataGenerator()->create_user(); // The primary admin is the one listed first in the 'siteadmins' config. set_config('siteadmins', implode(',', [$admin1->id, $admin2->id])); // Set the current user as the second admin (non-primary). $this->setUser($admin2); $this->assertTrue(api::can_create_data_deletion_request_for_self()); } /** * Test user can create data deletion request for themselves if they have * "tool/dataprivacy:requestdelete" capability. * * @throws coding_exception */ public function test_can_create_data_deletion_request_for_self_yes() { $this->resetAfterTest(); $userid = $this->getDataGenerator()->create_user()->id; $this->setUser($userid); $this->assertTrue(api::can_create_data_deletion_request_for_self()); } /** * Test user cannot create data deletion request for another user if they * don't have "tool/dataprivacy:requestdeleteforotheruser" capability. * * @throws coding_exception * @throws dml_exception */ public function test_can_create_data_deletion_request_for_other_no() { $this->resetAfterTest(); $userid = $this->getDataGenerator()->create_user()->id; $this->setUser($userid); $this->assertFalse(api::can_create_data_deletion_request_for_other()); } /** * Test user can create data deletion request for another user if they * don't have "tool/dataprivacy:requestdeleteforotheruser" capability. * * @throws coding_exception */ public function test_can_create_data_deletion_request_for_other_yes() { $this->resetAfterTest(); $userid = $this->getDataGenerator()->create_user()->id; $roleid = $this->getDataGenerator()->create_role(); $contextsystem = \context_system::instance(); assign_capability('tool/dataprivacy:requestdeleteforotheruser', CAP_ALLOW, $roleid, $contextsystem); role_assign($roleid, $userid, $contextsystem); $this->setUser($userid); $this->assertTrue(api::can_create_data_deletion_request_for_other($userid)); } /** * Check parents can create data deletion request for their children (unless the child is the primary admin), * but not other users. * * @throws coding_exception * @throws dml_exception */ public function test_can_create_data_deletion_request_for_children() { $this->resetAfterTest(); $parent = $this->getDataGenerator()->create_user(); $child = $this->getDataGenerator()->create_user(); $otheruser = $this->getDataGenerator()->create_user(); $contextsystem = \context_system::instance(); $parentrole = $this->getDataGenerator()->create_role(); assign_capability('tool/dataprivacy:makedatarequestsforchildren', CAP_ALLOW, $parentrole, $contextsystem); assign_capability('tool/dataprivacy:makedatadeletionrequestsforchildren', CAP_ALLOW, $parentrole, $contextsystem); role_assign($parentrole, $parent->id, \context_user::instance($child->id)); $this->setUser($parent); $this->assertTrue(api::can_create_data_deletion_request_for_children($child->id)); $this->assertFalse(api::can_create_data_deletion_request_for_children($otheruser->id)); // Now make child the primary admin, confirm parent can't make deletion request. set_config('siteadmins', $child->id); $this->assertFalse(api::can_create_data_deletion_request_for_children($child->id)); } /** * Data provider function for testing \tool_dataprivacy\api::queue_data_request_task(). * * @return array */ public function queue_data_request_task_provider() { return [ 'With user ID provided' => [true], 'Without user ID provided' => [false], ]; } /** * Test for \tool_dataprivacy\api::queue_data_request_task(). * * @dataProvider queue_data_request_task_provider * @param bool $withuserid */ public function test_queue_data_request_task(bool $withuserid) { $this->resetAfterTest(); $this->setAdminUser(); if ($withuserid) { $user = $this->getDataGenerator()->create_user(); api::queue_data_request_task(1, $user->id); $expecteduserid = $user->id; } else { api::queue_data_request_task(1); $expecteduserid = null; } // Test number of queued data request tasks. $datarequesttasks = manager::get_adhoc_tasks(process_data_request_task::class); $this->assertCount(1, $datarequesttasks); $requesttask = reset($datarequesttasks); $this->assertEquals($expecteduserid, $requesttask->get_userid()); } /** * Data provider for test_is_automatic_request_approval_on(). */ public function automatic_request_approval_setting_provider() { return [ 'Data export, not set' => [ 'automaticdataexportapproval', api::DATAREQUEST_TYPE_EXPORT, null, false ], 'Data export, turned on' => [ 'automaticdataexportapproval', api::DATAREQUEST_TYPE_EXPORT, true, true ], 'Data export, turned off' => [ 'automaticdataexportapproval', api::DATAREQUEST_TYPE_EXPORT, false, false ], 'Data deletion, not set' => [ 'automaticdatadeletionapproval', api::DATAREQUEST_TYPE_DELETE, null, false ], 'Data deletion, turned on' => [ 'automaticdatadeletionapproval', api::DATAREQUEST_TYPE_DELETE, true, true ], 'Data deletion, turned off' => [ 'automaticdatadeletionapproval', api::DATAREQUEST_TYPE_DELETE, false, false ], ]; } /** * Test for \tool_dataprivacy\api::is_automatic_request_approval_on(). * * @dataProvider automatic_request_approval_setting_provider * @param string $setting The automatic approval setting. * @param int $type The data request type. * @param bool $value The setting's value. * @param bool $expected The expected result. */ public function test_is_automatic_request_approval_on($setting, $type, $value, $expected) { $this->resetAfterTest(); if ($value !== null) { set_config($setting, $value, 'tool_dataprivacy'); } $this->assertEquals($expected, api::is_automatic_request_approval_on($type)); } } defaults.php 0000644 00000005551 15152701722 0007074 0 ustar 00 <?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/>. /** * This page lets users manage default purposes and categories. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once(__DIR__ . '/../../../config.php'); require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php'); require_login(null, false); $url = new \moodle_url('/admin/tool/dataprivacy/defaults.php'); $title = get_string('setdefaults', 'tool_dataprivacy'); \tool_dataprivacy\page_helper::setup($url, $title, 'dataregistry'); $mode = optional_param('mode', CONTEXT_COURSECAT, PARAM_INT); $classname = context_helper::get_class_for_level($mode); list($purposevar, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname); $purpose = get_config('tool_dataprivacy', $purposevar); $category = get_config('tool_dataprivacy', $categoryvar); $otherdefaults = []; if ($mode == CONTEXT_MODULE) { // Get activity module plugin info. $pluginmanager = core_plugin_manager::instance(); $modplugins = $pluginmanager->get_enabled_plugins('mod'); foreach ($modplugins as $name) { list($purposevar, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname, $name); $plugincategory = get_config('tool_dataprivacy', $categoryvar); $pluginpurpose = get_config('tool_dataprivacy', $purposevar); if ($plugincategory === false && $pluginpurpose === false) { // If no purpose and category has been set for this plugin, then there's no need to show this on the list. continue; } $displayname = $pluginmanager->plugin_name('mod_' . $name); $otherdefaults[$name] = (object)[ 'name' => $displayname, 'category' => $plugincategory, 'purpose' => $pluginpurpose, ]; } } $defaultspage = new \tool_dataprivacy\output\defaults_page($mode, $category, $purpose, $otherdefaults, true); $output = $PAGE->get_renderer('tool_dataprivacy'); echo $output->header(); echo $output->heading($title); echo $output->render_from_template('tool_dataprivacy/defaults_page', $defaultspage->export_for_template($output)); echo $output->footer(); templates/data_request_email.mustache 0000644 00000010405 15152701722 0014127 0 ustar 00 {{! 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/>. }} {{! @template tool_dataprivacy/data_request_email Email template for the data request. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * string dponame The name of the Data Protection Officer * string requestfor The user the request is being made for. * string requestedby The one making the request. * string requesttype The request type. * string requestdate The date the request was made. * string requestorigin The name of the site the request originates from. * string requestoriginurl The homepage of the site the request originates from. * string requestcomments Additional details regarding the request. * bool forself Whether the request has been made on behalf of another user or not. * string datarequestsurl The URL to the data requests page. Example context (json): { "dponame": "Eva Ferrer", "requestfor": "Oscar Olsen", "requestedby": "Angus Zhang", "requesttype": "Export user data", "requestdate": "31 January 2018", "requestorigin": "My Amazing Site", "requestoriginurl": "https://www.bestmoodlesiteever.com", "requestcomments": "Dear admin,<br/> I would like to request a copy of my son's user data. Thanks!", "forself": true, "datarequestsurl": "#" } }} <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <style> table.dataprivacy_email_table, .dataprivacy_email_table th, .dataprivacy_email_table td { border: 1px solid black; padding: 0.5em; } </style> <title>{{#str}}datarequestemailsubject, tool_dataprivacy, {{requesttype}}{{/str}}</title> </head> <body> <div> <p>{{#str}}emailsalutation, tool_dataprivacy, {{dponame}}{{/str}}</p> <p>{{#str}}requestemailintro, tool_dataprivacy{{/str}}</p> <table class="dataprivacy_email_table"> <tr> <th scope="row"> {{#str}}requesttype, tool_dataprivacy{{/str}} </th> <td> {{requesttype}} </td> </tr> <tr> <th scope="row"> {{#str}}requestfor, tool_dataprivacy{{/str}} </th> <td> {{requestfor}} </td> </tr> {{^forself}} <tr> <th scope="row"> {{#str}}requestby, tool_dataprivacy{{/str}} </th> <td> {{requestedby}} </td> </tr> {{/forself}} <tr> <th scope="row"> {{#str}}requestorigin, tool_dataprivacy{{/str}} </th> <td> <a href="{{requestoriginurl}}">{{{requestorigin}}}</a> </td> </tr> <tr> <th scope="row"> {{#str}}requestcomments, tool_dataprivacy{{/str}} </th> <td> {{{requestcomments}}} </td> </tr> <tr> <th scope="row"> {{#str}}daterequested, tool_dataprivacy{{/str}} </th> <td> {{requestdate}} </td> </tr> </table> <hr> <a href="{{datarequestsurl}}">{{#str}}viewrequest, tool_dataprivacy{{/str}}</a> </div> </body> </html> templates/data_requests_bulk_actions.mustache 0000644 00000003511 15152701722 0015700 0 ustar 00 {{! 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/>. }} {{! @template tool_dataprivacy/data_requests_bulk_actions Moodle template for the bulk action select element in the data requests page. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * options - Array of options for the select with value and name. * perpage - HTML content of the records per page select element. Example context (json): { "options": [ { "value": 1, "name": "Approve" }, { "value": 2, "name": "Deny" } ], "perpage" : "<div class='singleselect'></div>" } }} <div class="mt-1 d-inline-block w-100"> <div class="float-left"> <select id="bulk-action" class="select custom-select"> {{#options}} <option value="{{ value }}">{{ name }}</option> {{/options}} </select> <button class="btn btn-primary" id="confirm-bulk-action">{{# str}} confirm {{/ str}}</button> </div> <div class="float-right"> {{{ perpage }}} </div> </div> templates/form-user-selector-suggestion.mustache 0000644 00000002752 15152701722 0016227 0 ustar 00 {{! 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/>. }} {{! @template tool_dataprivacy/form-user-selector-suggestion Moodle template for the list of valid options in an autocomplate form element. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * fullname string Users full name * email string user email field Example context (json): { "fullname": "Admin User", "extrafields": [ { "name": "email", "value": "admin@example.com" }, { "name": "phone1", "value": "0123456789" } ] } }} <span> <span>{{fullname}}</span> {{#extrafields}} <span><small>{{{value}}}</small></span> {{/extrafields}} </span> templates/data_request_results_email.mustache 0000644 00000003344 15152701722 0015714 0 ustar 00 {{! 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/>. }} {{! @template tool_dataprivacy/data_request_results_email Email template for the data request processing results. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * string username The user's name. * string message The message to the user. * object downloadlink The action link context data for the download link (e.g. in case of user data export). Example context (json): { "username": "Eva Ferrer", "message": "Your data is ready for download. Enjoy!", "downloadlink": { "url": "#", "id": "test-id", "attributes": [ { "name": "title", "value": "Download here!" } ], "text": "Download here!" } } }} <div> <p>{{#str}}emailsalutation, tool_dataprivacy, {{username}}{{/str}}</p> <p>{{message}}</p> <p>{{#downloadlink}}{{> core/action_link}}{{/downloadlink}}</p> </div> templates/categories.mustache 0000644 00000005267 15152701722 0012436 0 ustar 00 {{! 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/>. }} {{! @template tool_dataprivacy/categories Manage categories. Classes required for JS: Data attributes required for JS: Context variables required for this template: * categories - array of objects * actions - array of actions (already in HTML). Example context (json): { "categoriesexist": 1, "categories": [ { "name" : "Category 1", "description": "<strong>Description 1</strong>", "actions": [ ] }, { "name" : "Category 2", "description": "<strong>Description 2</strong>", "actions": [ ] } ] } }} {{#navigation}} {{> core/action_link}} {{/navigation}} <div data-region="categories" class="mt-3 mb-1"> <div class="my-1"> <button class="btn btn-secondary" data-add-element="category" title="{{#str}}addcategory, tool_dataprivacy{{/str}}"> {{#pix}}t/add, moodle, {{#str}}addcategory, tool_dataprivacy{{/str}}{{/pix}} </button> </div> <table class="table table-striped table-hover"> <caption class="accesshide">{{#str}}categorieslist, tool_dataprivacy{{/str}}</caption> <thead> <tr> <th scope="col">{{#str}}name{{/str}}</th> <th scope="col" class="w-50">{{#str}}description{{/str}}</th> <th scope="col">{{#str}}actions{{/str}}</th> </tr> </thead> <tbody> {{#categories}} <tr data-categoryid="{{id}}"> <td>{{{name}}}</td> <td>{{{description}}}</td> <td> {{#actions}} {{> core/action_menu}} {{/actions}} </td> </tr> {{/categories}} </tbody> </table> {{^categories}} <p> {{#str}}nocategories, tool_dataprivacy{{/str}} </p> {{/categories}} </div> templates/delete_activity_defaults.mustache 0000644 00000002345 15152701722 0015350 0 ustar 00 {{! 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/>. }} {{! @template tool_dataprivacy/delete_activity_defaults Renders the confirmation. Classes required for JS: Data attributes required for JS: * none Context variables required for this template: * activityname String - The activity name. Example context (json): { "activityname": "Assignment" } }} <div class="alert alert-warning" role="alert"> {{#str}}defaultswarninginfo, tool_dataprivacy{{/str}} </div> <div> {{#str}}deletedefaultsconfirmation, tool_dataprivacy, {{activityname}}{{/str}} </div> templates/defaults_page.mustache 0000644 00000013445 15152701722 0013111 0 ustar 00 {{! 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/>. }} {{! @template tool_dataprivacy/defaults_page Manage data registry defaults. Classes required for JS: Data attributes required for JS: Context variables required for this template: * contextlevel Number - The context level. * modecoursecat Boolean - Whether we're displaying defaults for course categories. * modecourse Boolean - Whether we're displaying defaults for courses. * modemodule Boolean - Whether we're displaying defaults for activity modules. * modeblock Boolean - Whether we're displaying defaults for blocks. * coursecaturl String - The URL for the course category defaults tab. * courseurl String - The URL for the course defaults tab. * moduleurl String - The URL for the activity module defaults tab. * blockurl String - The URL for the block defaults tab. * purposeid Number - The purpose ID for this context level. * canedit Boolean - Whether this is being rendered for editing purposes. * categoryid Number - The ID of the default category for this context level. * purposeid Number - The ID of the default purpose for this context level. * category String - The category name. * purpose String - The purpose name. * otherdefaults Array - An array containing the defaults for the activity modules. Example context (json): { "contextlevel": 70, "modecoursecat": false, "modecourse": false, "modemodule": true, "modeblock": false, "coursecaturl": "#", "courseurl": "#", "moduleurl": "#", "blockurl": "#", "category": "Awesome default category", "purpose": "Awesome default purpose", "canedit": true, "otherdefaults": [ { "name": "Assignment", "category": "Category for activity modules", "purpose": "Assessments" }, { "name": "Forum", "category": "Category for activity modules", "purpose": "Social interactions" } ] } }} <div class="card"> <div class="card-header"> <ul class="nav nav-tabs card-header-tabs"> <li class="nav-item"> <a class="nav-link {{#modecoursecat}}active{{/modecoursecat}}" href="{{coursecaturl}}">{{#str}}categories{{/str}}</a> </li> <li class="nav-item"> <a class="nav-link {{#modecourse}}active{{/modecourse}}" href="{{courseurl}}">{{#str}}courses{{/str}}</a> </li> <li class="nav-item"> <a class="nav-link {{#modemodule}}active{{/modemodule}}" href="{{moduleurl}}">{{#str}}activitymodules{{/str}}</a> </li> <li class="nav-item"> <a class="nav-link {{#modeblock}}active{{/modeblock}}" href="{{blockurl}}">{{#str}}blocks{{/str}}</a> </li> </ul> </div> <div class="card-body context-level-view"> <div class="alert alert-primary" role="alert"> {{#str}}defaultsinfo, tool_dataprivacy{{/str}} </div> <h4 class="card-title" id="defaults-header"> {{#modecoursecat}}{{#str}}categories{{/str}}{{/modecoursecat}} {{#modecourse}}{{#str}}courses{{/str}}{{/modecourse}} {{#modemodule}}{{#str}}activitymodules{{/str}}{{/modemodule}} {{#modeblock}}{{#str}}blocks{{/str}}{{/modeblock}} </h4> <div> {{> tool_dataprivacy/defaults_display}} {{#canedit}} {{#modemodule}} <button class="btn btn-primary" data-action="new-activity-defaults" data-contextlevel="{{contextlevel}}"> {{#str}}addnewdefaults, tool_dataprivacy{{/str}} </button> {{/modemodule}} {{/canedit}} {{#modemodule}} <table class="mt-1 table table-striped"> <thead> <tr> <th> </th> <th scope="col">{{#str}}category, tool_dataprivacy{{/str}}</th> <th scope="col">{{#str}}purpose, tool_dataprivacy{{/str}}</th> {{#canedit}} <th scope="col">{{#str}}actions{{/str}}</th> {{/canedit}} </tr> </thead> <tbody> {{#otherdefaults}} <tr> <th scope="row">{{name}}</th> <td>{{category}}</td> <td>{{purpose}}</td> {{#canedit}} <td> {{#actions}} {{> core/action_menu_link}} {{/actions}} </td> {{/canedit}} </tr> {{/otherdefaults}} </tbody> </table> {{/modemodule}} </div> </div> </div> {{#js}} // Initialise the JS. require(['tool_dataprivacy/defaultsactions'], function(ActionsMod) { ActionsMod.init(); }); {{/js}} templates/my_data_requests.mustache 0000644 00000013720 15152701722 0013653 0 ustar 00 {{! 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 comments. You should have received a copy of the GNU General Public License along with Moodle. If not, see <http://www.gnu.org/licenses/>. }} {{! @template tool_dataprivacy/my_data_requests A user's data requests page. Classes required for JS: * requestactions Data attributes required for JS: * none Context variables required for this template: * requests - Array of data requests. Example context (json): { "requests": [ { "id": 1, "typename" : "Data export", "comments": "I would like to download all of my daughter's personal data", "statuslabelclass": "badge-secondary", "statuslabel": "Pending", "timecreated" : 1517902087, "requestedbyuser" : { "fullname": "Martha Smith", "profileurl": "#" } }, { "id": 2, "typename" : "Data export", "comments": "Give me all of the information you have about me, or else...", "statuslabelclass": "badge-warning", "statuslabel": "Awaiting completion", "timecreated" : 1517902087, "requestedbyuser" : { "fullname": "Martha Smith", "profileurl": "#" } }, { "id": 3, "typename" : "Data deletion", "comments": "Please delete all of my son's personal data.", "statuslabelclass": "badge-success", "statuslabel": "Deleted", "timecreated" : 1517902087, "requestedbyuser" : { "fullname": "Martha Smith", "profileurl": "#" } }, { "id": 4, "typename" : "Data deletion", "comments": "Delete my data or I'm coming for you...", "statuslabelclass": "label-danger", "statuslabel": "Rejected", "timecreated" : 1517902087, "requestedbyuser" : { "fullname": "Martha Smith", "profileurl": "#" } }, { "id": 5, "typename" : "Data export", "comments": "Please let me download my data", "statuslabelclass": "badge-info", "statuslabel": "Processing", "timecreated" : 1517902087, "requestedbyuser" : { "fullname": "Martha Smith", "profileurl": "#" } }, { "id": 6, "typename" : "Data export", "comments": "Please let me download my data", "statuslabelclass": "label", "statuslabel": "Expired", "statuslabeltitle": "Download has expired. Submit a new request if you wish to export your personal data.", "timecreated" : 1517902087, "requestedbyuser" : { "fullname": "Martha Smith", "profileurl": "#" } } ] } }} {{#httpsite}} {{> core/notification_warning}} {{/httpsite}} <div data-region="datarequests"> <div class="mt-1 mb-1"> <a href="{{newdatarequesturl}}" class="btn btn-primary" data-action="new-request"> {{#str}}newrequest, tool_dataprivacy{{/str}} </a> </div> <table class="generaltable fullwidth"> <thead> <tr> <th scope="col">{{#str}}requesttype, tool_dataprivacy{{/str}}</th> <th scope="col">{{#str}}daterequested, tool_dataprivacy{{/str}}</th> <th scope="col">{{#str}}requestby, tool_dataprivacy{{/str}}</th> <th scope="col">{{#str}}requeststatus, tool_dataprivacy{{/str}}</th> <th scope="col" colspan="2">{{#str}}message, tool_dataprivacy{{/str}}</th> </tr> </thead> <tbody> {{#requests}} <tr {{! }} data-region="request-node"{{! }} data-id="{{id}}"{{! }} data-type="{{type}}"{{! }} data-status="{{status}}"{{! }}> <td>{{typename}}</td> <td>{{#userdate}} {{timecreated}}, {{#str}} strftimedatetime, core_langconfig {{/str}} {{/userdate}}</td> <td><a href="{{requestedbyuser.profileurl}}" title="{{#str}}viewprofile{{/str}}">{{requestedbyuser.fullname}}</a></td> <td> <span class="badge {{statuslabelclass}}" title="{{statuslabeltitle}}">{{statuslabel}}</span> </td> <td>{{comments}}</td> <td> {{#actions}} {{> core/action_menu}} {{/actions}} </td> </tr> {{/requests}} {{^requests}} <tr> <td class="text-muted" colspan="5"> {{#str}}nopersonaldatarequests, tool_dataprivacy{{/str}} </td> </tr> {{/requests}} </tbody> </table> </div> {{#js}} // Initialise the JS. require(['tool_dataprivacy/myrequestactions'], function(ActionsMod) { ActionsMod.init(); }); {{/js}} templates/request_details.mustache 0000644 00000007371 15152701722 0013504 0 ustar 00 {{! 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 comments. You should have received a copy of the GNU General Public License along with Moodle. If not, see <http://www.gnu.org/licenses/>. }} {{! @template tool_dataprivacy/request_details Data request details Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * foruser Object The context data provided by core_user/external/user_summary_exporter. * reviewurl String The URL for the Review request link. * timecreated Number The timestamp when the request was created. * statuslabel String The text equivalent of the status. * statuslabelclass String The class to be used for rendering the status text. * messagehtml String The HTML version of the request message. Example context (json): { "foruser" : { "fullname": "Martha Smith", "email": "martha@example.com", "profileurl": "#", "profileimageurl": "https://randomuser.me/api/portraits/women/60.jpg" }, "canreview": true, "reviewurl": "#", "timecreated": 1517561224, "requestedbyuser" : { "fullname": "Martha Smith", "profileurl": "#" }, "statuslabel": "Pending", "statuslabelclass": "badge-secondary", "messagehtml": "<p>Hello,</p><p>I would like to download all of my personal data.</p><p>Thanks!</p>" } }} <div class="container" data-requestid="{{id}}"> <div class="media"> <div class="media-left"> <img class="userpicture" src="{{foruser.profileimageurl}}" alt="{{#str}}pictureof, moodle, {{foruser.fullname}}{{/str}}" title="{{#str}}pictureof, moodle, {{foruser.fullname}}{{/str}}" /> </div> <div class="media-body"> <h4 class="mt-0 mb-1"> <a href="{{foruser.profileurl}}" title="{{#str}}viewprofile{{/str}}">{{foruser.fullname}}</a> </h4> <a href="mailto:{{foruser.email}}">{{foruser.email}}</a> <div class="clearfix mt-1 mb-1"> <span class="float-left mr-1"> <strong>{{#str}}daterequesteddetail, tool_dataprivacy{{/str}}</strong> {{#userdate}} {{timecreated}}, {{#str}} strftimedatetime, core_langconfig {{/str}} {{/userdate}} </span> <span class="float-left mr-1"> <strong>{{#str}}statusdetail, tool_dataprivacy{{/str}}</strong> <span class="badge {{statuslabelclass}}">{{statuslabel}}</span> </span> <span class="float-left mr-1"> <strong>{{#str}}requestbydetail, tool_dataprivacy{{/str}}</strong> <span><a href="{{requestedbyuser.profileurl}}" title="{{#str}}viewprofile{{/str}}">{{requestedbyuser.fullname}}</a></span> </span> </div> {{#canreview}} <!--a href="{{reviewurl}}" class="btn btn-secondary">{{#str}}reviewdata, tool_dataprivacy{{/str}}</a--> {{/canreview}} </div> </div> <hr> <div class="mb-1"> <strong>{{#str}}messagelabel, tool_dataprivacy{{/str}}</strong> </div> <div> {{{messagehtml}}} </div> </div> templates/data_requests.mustache 0000644 00000004650 15152701722 0013150 0 ustar 00 {{! 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 comments. You should have received a copy of the GNU General Public License along with Moodle. If not, see <http://www.gnu.org/licenses/>. }} {{! @template tool_dataprivacy/data_requests Data requests page. Classes required for JS: * requestactions Data attributes required for JS: * none Context variables required for this template: * newdatarequesturl string The URL pointing to the data request creation page. * datarequests string The HTML of the data requests table. Example context (json): { "newdatarequesturl": "#", "datarequests": "<table><tr><td>This is the table where the list of data requests will be rendered</td></tr></table>", "filter": { "action": "#", "filteroptions": [ { "value": "1", "label": "Option 1" }, { "value": "2", "label": "Option 2", "selected": true }, { "value": "3", "label": "Option 3", "selected": true } ] } } }} {{#httpsite}} {{> core/notification_warning}} {{/httpsite}} <div data-region="datarequests"> <div class="mt-1 mb-1"> <div class="float-right"> <a href="{{newdatarequesturl}}" class="btn btn-primary" data-action="new-request"> {{#str}}newrequest, tool_dataprivacy{{/str}} </a> </div> {{#filter}} {{>tool_dataprivacy/request_filter}} {{/filter}} </div> <div class="mt-1 mb-1" data-region="data-requests-table"> {{{datarequests}}} </div> </div> {{#js}} // Initialise the JS. require(['tool_dataprivacy/requestactions'], function(ActionsMod) { new ActionsMod(); }); {{/js}} templates/component_status.mustache 0000644 00000010144 15152701722 0013704 0 ustar 00 {{! 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 comments. You should have received a copy of the GNU General Public License along with Moodle. If not, see <http://www.gnu.org/licenses/>. }} {{! @template tool_dataprivacy/component_status Data registry main page. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * none Example context (json): { "compliant" : "True", "raw_component" : "core_comment", "component" : "Core comment", "external" : "True", "metadata" : { "name" : "comments", "type" : "database_table", "summary" : "Stores comments of users", "fields" : { "field_name" : "content", "field_summary" : "Stores the text of the content." } } } }} <div class="container-fluid"> <hr /> <div class="row"> {{#compliant}} <a class="component-expand pl-5" data-component="{{raw_component}}" href='#'> <h4 class=" d-inline pl-5 " id="{{raw_component}}">{{#pix}}t/collapsed, moodle, {{#str}}expandplugin, tool_dataprivacy{{/str}}{{/pix}}{{component}}</h4> </a> {{/compliant}} {{^compliant}} <h4 class="d-inline pl-6 " id="{{raw_component}}">{{component}}</h4> <span>{{#pix}}i/risk_xss, moodle, {{#str}}requiresattention, tool_dataprivacy{{/str}}{{/pix}}</span> {{/compliant}} {{#external}} <span class="badge badge-pill badge-notice">{{#str}}external, tool_dataprivacy{{/str}}</span> {{/external}} {{#deprecated}} <span class="badge badge-pill badge-warning">{{#str}}deprecated, tool_dataprivacy{{/str}}</span> {{/deprecated}} {{#userlistnoncompliance}} <span class="badge badge-pill badge-warning">{{#str}}userlistnoncompliant, tool_dataprivacy{{/str}}</span> {{/userlistnoncompliance}} </div> {{#compliant}} <div class="hide" data-section="{{raw_component}}" aria-expanded="false" role="contentinfo"> {{#metadata}} <hr /> <div class="pl-6"> <dl class="row"> <dt class="col-3"> {{#link}} <a href="#{{name}}"><strong style="word-wrap:break-word">{{name}}</strong></a> {{/link}} {{^link}} <strong style="word-wrap:break-word">{{name}}</strong> {{/link}} <div class="small text-muted" style="word-wrap:break-word">{{type}}</div> </dt> <dd class="col-9">{{summary}}</dd> </dl> <dl> {{#fields}} <div class="row"> <dt class="col-3 font-weight-normal" style="word-wrap:break-word">{{field_name}}</dt> <dd class="col-9">{{field_summary}}</dd> </div> {{/fields}} </dl> </div> {{/metadata}} {{#nullprovider}} <hr /> <div class="pl-6"> <div class="row"> <div class="col-12"> {{nullprovider}} </div> </div> </div> {{/nullprovider}} </div> {{/compliant}} </div> templates/request_filter.mustache 0000644 00000004364 15152701722 0013343 0 ustar 00 {{! 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/>. }} {{! @template tool_dataprivacy/request_filter Template for the request filter element. Context variables required for this template: * action string - The action URL for the form. * filteroptions - Array of filter options. * value string - The option value. * label string - The option label. * selected boolean - Whether the option is selected Example context (json): { "action": "#", "filteroptions": [ { "value": "1", "label": "Option 1" }, { "value": "2", "label": "Option 2", "selected": true }, { "value": "3", "label": "Option 3", "selected": true }, { "value": "4", "label": "Option 4" } ] } }} <form method="post" action="{{action}}" class="mb-1" role="search" id="request_filter_form"> <label for="request-filters" class="sr-only">{{#str}}filters{{/str}}</label> <select name="request-filters[]" id="request-filters" multiple="multiple" class="form-autocomplete-original-select"> {{#filteroptions}} <option value="{{value}}" {{#selected}}selected="selected"{{/selected}}>{{{label}}}</option> {{/filteroptions}} </select> <input type="hidden" id="filters-cleared" name="filters-cleared" value="0" /> </form> {{#js}} require(['tool_dataprivacy/request_filter'], function(Filter) { Filter.init(); }); {{/js}} templates/data_deletion.mustache 0000644 00000006201 15152701722 0013072 0 ustar 00 {{! 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 comments. You should have received a copy of the GNU General Public License along with Moodle. If not, see <http://www.gnu.org/licenses/>. }} {{! @template tool_dataprivacy/data_deletion Data deletion page. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * filter - The context data for single_select element that has the options for the table filter. * expiredcontexts - The HTML for the table of expired contexts. Example context (json): { "filter": { "name": "filter", "method": "get", "action": "#", "classes": "singleselect", "label": "", "disabled": false, "title": null, "formid": "single_select_f5ac5e42bb035319", "id": "single_select5ac5e42bb035320", "params":[], "options":[ {"value": 50, "name": "Course", "selected": true, "optgroup": false}, {"value": 70, "name": "Activities and resources", "selected":false, "optgroup": false}, {"value": 80, "name": "Blocks", "selected": false, "optgroup": false} ], "labelattributes": [], "helpicon": false }, "expiredcontexts": "<table class='table'><tbody><tr><td>This is the table that will contain the list of expired contexts</td></tr></tbody></table>" } }} <div class="container-fluid" data-region="data-deletion"> <div class="row" data-region="top-nav"> <div class="alert alert-info"> {{#str}}datadeletionpagehelp, tool_dataprivacy{{/str}} </div> <div class="float-left"> {{#filter}} {{> core/single_select}} {{/filter}} </div> {{#existingcontexts}} <div class="float-right"> <button data-action="markfordeletion" class="btn btn-secondary">{{#str}}deleteselected, moodle{{/str}}</button> </div> {{/existingcontexts}} </div> <div class="row mt-1 mb-1" data-region="expired-contexts-table"> {{{expiredcontexts}}} </div> <div class="row" data-region="bottom-nav"> {{#existingcontexts}} <div class="float-right"> <button data-action="markfordeletion" class="btn btn-secondary">{{#str}}deleteselected, moodle{{/str}}</button> </div> {{/existingcontexts}} </div> </div> {{#js}} // Initialise the JS. require(['tool_dataprivacy/data_deletion'], function(DataDeletion) { new DataDeletion(); }); {{/js}} templates/category_purpose_form.mustache 0000644 00000007710 15152701722 0014721 0 ustar 00 {{! 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/>. }} {{! @template tool_dataprivacy/category_purpose_form Manage data registry defaults. Classes required for JS: Data attributes required for JS: Context variables required for this template: * actionurl String The action URL. * contextlevel Number - The context level. * modemodule Boolean - Whether to display the activity select element. * activityoptions Array - An array of objects for the activity select element. * categoryoptions Array - An array of objects for the category select element. * purposeoptions Array - An array of objects for the purpose select element. Example context (json): { "actionurl": "#", "contextlevel": 70, "newactivitydefaults": true, "modemodule": true, "activityoptions": [ { "name": "assign", "displayname": "Assignment" }, { "name": "forum", "displayname": "Forum", "selected": true }, { "name": "lesson", "displayname": "Lesson" }, { "name": "quiz", "displayname": "Quiz" } ], "categoryoptions": [ { "id": 1, "name": "Category 1" }, { "id": 2, "name": "Category 2", "selected": true }, { "id": 3, "name": "Category 3" } ], "purposeoptions": [ { "id": 1, "name": "Purpose 1" }, { "id": 2, "name": "Purpose 2" }, { "id": 3, "name": "Purpose 3", "selected": true } ] } }} <div class="alert alert-warning" role="alert"> {{#str}}defaultswarninginfo, tool_dataprivacy{{/str}} </div> <form method="post" action="{{actionurl}}" id="category_purpose_form"> <input type="hidden" value="{{contextlevel}}" id="contextlevel" /> {{#modemodule}} <div class="form-group"> <label for="activity">{{#str}}activitymodule{{/str}}</label> {{^newactivitydefaults}} <input type="hidden" id="activity" value="{{#activityoptions}}{{#selected}}{{name}}{{/selected}}{{/activityoptions}}" /> {{/newactivitydefaults}} <select class="form-control" {{#newactivitydefaults}}id="activity" {{/newactivitydefaults}}{{^newactivitydefaults}}disabled{{/newactivitydefaults}}> {{#activityoptions}} <option value="{{name}}" {{#selected}}selected{{/selected}}>{{displayname}}</option> {{/activityoptions}} </select> </div> {{/modemodule}} <div class="form-group"> <label for="category">{{#str}}category, tool_dataprivacy{{/str}}</label> <select class="form-control" id="category"> {{#categoryoptions}} <option value="{{id}}" {{#selected}}selected{{/selected}}>{{name}}</option> {{/categoryoptions}} </select> </div> <div class="form-group"> <label for="purpose">{{#str}}purpose, tool_dataprivacy{{/str}}</label> <select class="form-control" id="purpose"> {{#purposeoptions}} <option value="{{id}}" {{#selected}}selected{{/selected}}>{{name}}</option> {{/purposeoptions}} </select> </div> <div class="form-check"> <input class="form-check-input" type="checkbox" value="1" id="override"> <label class="form-check-label" for="override"> {{#str}}overrideinstances, tool_dataprivacy{{/str}} </label> </div> </form> templates/defaults_display.mustache 0000644 00000004663 15152701722 0013644 0 ustar 00 {{! 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/>. }} {{! @template tool_dataprivacy/defaults_display Renders the current default category and purpose. Classes required for JS: Data attributes required for JS: Context variables required for this template: * contextlevel Number - The context level. * categoryid Number - The category ID. * purposeid Number - The purpose ID. * category String - The category name. * purpose String - The purpose name. * canedit Boolean - Whether this is being rendered for editing purposes. Example context (json): { "category": "Awesome default category", "categoryid": 1, "purpose": "Awesome default purpose", "purposeid": 2, "canedit": true, "contextlevel": 70 } }} <div class="row rtl-compatible mt-1 mb-1"> <div class="col-md-9"> <div class="row rtl-compatible mt-1 mb-1"> <div class="col-md-3"> <strong>{{#str}}category, tool_dataprivacy{{/str}}</strong> </div> <div class="col-md-9"> {{category}} </div> </div> <div class="row rtl-compatible mt-1 mb-1"> <div class="col-md-3"> <strong>{{#str}}purpose, tool_dataprivacy{{/str}}</strong> </div> <div class="col-md-9"> {{purpose}} </div> </div> </div> <div class="col-md-3 align-self-center"> {{#canedit}} <button class="btn btn-secondary" {{! }}data-action="edit-level-defaults" {{! }}data-contextlevel="{{contextlevel}}" {{! }}data-category="{{categoryid}}" {{! }}data-purpose="{{purposeid}}"> {{#str}}edit{{/str}} </button> {{/canedit}} </div> </div> templates/context_tree_node.mustache 0000644 00000003342 15152701722 0014011 0 ustar 00 {{! 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/>. }} {{! @template tool_dataprivacy/context_tree_branch A tree branch. Classes required for JS: Data attributes required for JS: Context variables required for this template: Example context (json): { } }} <a class="nav-link {{#active}}active{{/active}}" href="#" data-context-tree-node="1" {{#contextlevel}} data-contextlevel="{{.}}" {{/contextlevel}} {{#contextid}} data-contextid="{{.}}" {{/contextid}} {{#expandcontextid}} data-expandcontextid="{{.}}" {{/expandcontextid}} {{#expandelement}} data-expandelement="{{.}}" {{/expandelement}} data-expanded="{{expanded}}"> <i class="fa {{#expandelement}}{{#expanded}}fa-minus{{/expanded}}{{/expandelement}} {{#expandelement}}{{^expanded}}fa-plus{{/expanded}}{{/expandelement}} {{^expandelement}}{{#expanded}}fa-folder-open{{/expanded}}{{/expandelement}} {{^expandelement}}{{^expanded}}fa-file{{/expanded}}{{/expandelement}}" > </i> {{text}} </a> {{> tool_dataprivacy/context_tree_branches}} templates/data_registry.mustache 0000644 00000003454 15152701722 0013146 0 ustar 00 {{! 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 comments. You should have received a copy of the GNU General Public License along with Moodle. If not, see <http://www.gnu.org/licenses/>. }} {{! @template tool_dataprivacy/data_registry Data registry main page. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * none Example context (json): { } }} <div class="data-registry"> <div class="top-nav d-flex"> {{#defaultsbutton}} {{> core/action_link}} {{/defaultsbutton}} {{#actions}} {{> core/action_menu}} {{/actions}} </div> {{#info}} <div class="mt-1"> {{> core/notification_info}} </div> {{/info}} {{#nosystemdefaults}} <div class="mt-1"> {{> core/notification_warning}} </div> {{/nosystemdefaults}} <div class="container-fluid mt-2"> <div class="row"> <div class="col-md-4 pl-0 nav-pills context-tree"> {{#tree}} {{> tool_dataprivacy/context_tree_node}} {{/tree}} </div> <div class="col-md-8" id="context-form-container"> </div> </div> </div> </div> templates/summary.mustache 0000644 00000011072 15152701722 0011775 0 ustar 00 {{! 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/>. }} {{! @template tool_dataprivacy/summary Summary Classes required for JS: Data attributes required for JS: Context variables required for this template: Example context (json): { "contexts": [ { "contextname": "Site", "category": { "name": "Test category", "description": "<p>Description for category</p>" }, "purpose": { "name": "Test purpose", "description": "<p>Description for purpose</p>", "lawfulbases": "gdpr_art_6_1_c", "sensitivedatareasons": "gdpr_art_9_2_f", "formattedlawfulbases": [ { "name": "Lawful base 1(a)", "description": "We need your information" }, { "name": "Lawful base 1(b)", "description": "We really do need your information" } ], "formattedsensitivedatareasons": [ { "name": "Sensitive data reason number 1", "description": "Number 1" }, { "name": "Sensitive data reason number 1", "description": "Number 2" } ], "formattedretentionperiod": "10 Years" } } ] } }} <h2>{{#str}}dataretentionsummary, tool_dataprivacy{{/str}}</h2> <p>{{#str}}dataretentionexplanation, tool_dataprivacy{{/str}}</p> <div> {{#contexts}} <div class="card mb-3"> <div class="card-header"><h3>{{contextname}}</h3></div> <div class="card-body pl-5 pr-5"> {{#category.name}} <h4>{{#str}}category, tool_dataprivacy{{/str}}</h4> <dl> <dt>{{category.name}}</dt> <dd>{{{category.description}}}</dd> </dl> <hr /> {{/category.name}} <h4>{{#str}}purpose, tool_dataprivacy{{/str}}</h4> <dl> <dt>{{purpose.name}}</dt> <dd>{{{purpose.description}}}</dd> <dt>{{#str}}retentionperiod, tool_dataprivacy{{/str}}</dt> <dd>{{purpose.formattedretentionperiod}}</dd> </dl> {{#purpose.lawfulbases}} <table class="table table-bordered"> <thead><tr><th colspan="2">{{#str}}lawfulbases, tool_dataprivacy{{/str}}</th></tr></thead> <tbody> {{#purpose.formattedlawfulbases}} <tr> <td>{{name}}</td> <td>{{description}}</td> </tr> {{/purpose.formattedlawfulbases}} </tbody> </table> {{/purpose.lawfulbases}} {{#purpose.sensitivedatareasons}} <table class="table table-bordered"> <thead><tr><th colspan="2">{{#str}}sensitivedatareasons, tool_dataprivacy{{/str}}</th></tr></thead> <tbody> {{#purpose.formattedsensitivedatareasons}} <tr> <td>{{name}}</td> <td>{{description}}</td> </tr> {{/purpose.formattedsensitivedatareasons}} </tbody> </table> {{/purpose.sensitivedatareasons}} </div> </div> {{/contexts}} </div> templates/context_tree_branches.mustache 0000644 00000002037 15152701722 0014651 0 ustar 00 {{! 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/>. }} {{! @template tool_dataprivacy/context_tree Context tree. Classes required for JS: Data attributes required for JS: Context variables required for this template: Example context (json): { } }} {{#branches}} <nav class="nav-pills flex-column"> {{> tool_dataprivacy/context_tree_node}} </nav> {{/branches}} templates/purposes.mustache 0000644 00000011752 15152701722 0012165 0 ustar 00 {{! 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/>. }} {{! @template tool_dataprivacy/purposes Manage purposes. Classes required for JS: Data attributes required for JS: Context variables required for this template: * purposes - array of objects * actions - array of actions (already in HTML). Example context (json): { "purposesexist": 1, "purposes": [ { "name" : "Purpose 1", "description": "<strong>Purpose 1 description</strong>", "retentionperiod": 86400, "protected": 1, "formattedretentionperiod": "1 day", "actions": [ ] }, { "name" : "Purpose 2", "description": "<strong>Purpose 2 description</strong>", "retentionperiod": 7200, "protected": 0, "formattedretentionperiod": "2 hours", "actions": [ ] } ] } }} {{#navigation}} <div class="mb-1"> {{> core/action_link}} </div> {{/navigation}} <p> {{#str}}purposeoverview, tool_dataprivacy{{/str}} </p> <div data-region="purposes" class="mb-1"> <div class="my-1"> <button class="btn btn-secondary" data-add-element="purpose" title="{{#str}}addpurpose, tool_dataprivacy{{/str}}"> {{#pix}}t/add, moodle, {{#str}}addpurpose, tool_dataprivacy{{/str}}{{/pix}} </button> </div> <table class="table table-striped table-hover"> <caption class="accesshide">{{#str}}purposeslist, tool_dataprivacy{{/str}}</caption> <thead> <tr> <th scope="col" class="w-25">{{#str}}name{{/str}}</th> <th scope="col">{{#str}}lawfulbases, tool_dataprivacy{{/str}}</th> <th scope="col">{{#str}}sensitivedatareasons, tool_dataprivacy{{/str}}</th> <th scope="col">{{#str}}retentionperiod, tool_dataprivacy{{/str}}</th> <th scope="col">{{#str}}protected, tool_dataprivacy{{/str}}</th> <th scope="col">{{#str}}roleoverrides, tool_dataprivacy{{/str}}</th> <th scope="col">{{#str}}actions{{/str}}</th> </tr> </thead> <tbody> {{#purposes}} <tr data-purposeid="{{id}}"> <td> <dl> <dt> {{{name}}} </dt> <dd> {{{description}}} </dd> </dl> </td> <td> <ul class="list-unstyled"> {{#formattedlawfulbases}} <li> <span>{{name}}{{# pix }} i/info, core, {{description}} {{/ pix }}</span> </li> {{/formattedlawfulbases}} </ul> </td> <td> <ul class="list-unstyled"> {{#formattedsensitivedatareasons}} <li> <span>{{name}}{{# pix }} i/info, core, {{description}} {{/ pix }}</span> </li> {{/formattedsensitivedatareasons}} </ul> </td> <td>{{formattedretentionperiod}}</td> <td> {{#protected}} {{#pix}}i/checked, core, {{#str}}yes{{/str}}{{/pix}} {{/protected}} {{^protected}} {{#str}}no{{/str}} {{/protected}} </td> <td> {{#roleoverrides}} {{#str}}yes{{/str}} {{/roleoverrides}} {{^roleoverrides}} {{#str}}no{{/str}} {{/roleoverrides}} </td> <td> {{#actions}} {{> core/action_menu}} {{/actions}} </td> </tr> {{/purposes}} </tbody> </table> {{^purposes}} <p> {{#str}}nopurposes, tool_dataprivacy{{/str}} </p> {{/purposes}} </div> templates/data_registry_compliance.mustache 0000644 00000007220 15152701722 0015333 0 ustar 00 {{! 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 comments. You should have received a copy of the GNU General Public License along with Moodle. If not, see <http://www.gnu.org/licenses/>. }} {{! @template tool_dataprivacy/data_registry_compliance Data registry main page. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * none Example context (json): { "types" : { "plugin_type_raw" : "mod", "plugin_type" : "Activities and Modules" } } }} <div class="dataprivacy-main"> <h2>{{#str}}pluginregistrytitle, tool_dataprivacy{{/str}}</h2> <hr /> <p><strong>{{#str}}explanationtitle, tool_dataprivacy{{/str}}</strong></p> <dl> <dt>{{#pix}}i/risk_xss, moodle, {{#str}}requiresattention, tool_dataprivacy{{/str}}{{/pix}}</dt> <dd>{{#str}}requiresattentionexplanation, tool_dataprivacy{{/str}}</dd> <dt><span class="badge badge-pill badge-notice">{{#str}}external, tool_dataprivacy{{/str}}</span></dt> <dd>{{#str}}externalexplanation, tool_dataprivacy{{/str}}</dd> <dt><span class="badge badge-pill badge-warning">{{#str}}deprecated, tool_dataprivacy{{/str}}</span></dt> <dd>{{#str}}deprecatedexplanation, tool_dataprivacy{{/str}}</dd> <dt><span class="badge badge-pill badge-warning">{{#str}}userlistnoncompliant, tool_dataprivacy{{/str}}</span></dt> <dd>{{#str}}userlistexplanation, tool_dataprivacy{{/str}}</dd> </dl> <hr /> <div class="clearfix"><a class="tool_dataprivacy-expand-all float-right" href="#" data-visibility-state='visible'>{{#str}}visible, tool_dataprivacy{{/str}}</a></div> {{#types}} <div> <div> <a class="type-expand" href='#' data-plugin="{{plugin_type_raw}}"> <h3 id="{{plugin_type_raw}}">{{#pix}}t/collapsed, moodle, {{#str}}expandplugintype, tool_dataprivacy{{/str}}{{/pix}}{{plugin_type}}</h3> </a> </div> <div class="hide pb-3" data-plugintarget="{{plugin_type_raw}}" aria-expanded="false" role="contentinfo"> {{#plugins}} {{> tool_dataprivacy/component_status}} {{/plugins}} </div> </div> {{/types}} </div> {{#js}} require(['jquery', 'tool_dataprivacy/expand_contract'], function($, ec) { $('.type-expand').click(function(e) { e.preventDefault(); e.stopPropagation(); var thisnode = $(this); var plugin = thisnode.data('plugin'); var metadata = $('[data-plugintarget=\'' + plugin + '\']'); ec.expandCollapse(metadata, thisnode); }); $('.component-expand').click(function(e) { e.preventDefault(); e.stopPropagation(); var thisnode = $(this); var plugin = thisnode.data('component'); var metadata = $('[data-section=\'' + plugin + '\']'); ec.expandCollapse(metadata, thisnode); }); $('.tool_dataprivacy-expand-all').click(function(e) { e.preventDefault(); e.stopPropagation(); var nextstate = $(this).data('visibilityState'); ec.expandCollapseAll(nextstate); }); }); {{/js}} templates/data_request_modal.mustache 0000644 00000003071 15152701722 0014135 0 ustar 00 {{! 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 comments. You should have received a copy of the GNU General Public License along with Moodle. If not, see <http://www.gnu.org/licenses/>. }} {{! @template tool_dataprivacy/data_request_modal Data request details Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * none Example context (json): { "title": "Data request modal title" } }} {{< core/modal }} {{$footer}} {{#approvedeny}} <button type="button" class="btn btn-primary" data-action="approve">{{#str}} approve, tool_dataprivacy {{/str}}</button> <button type="button" class="btn btn-secondary" data-action="deny">{{#str}} deny, tool_dataprivacy {{/str}}</button> {{/approvedeny}} {{#canmarkcomplete}} <button type="button" class="btn btn-primary" data-action="complete">{{#str}} markcomplete, tool_dataprivacy {{/str}}</button> {{/canmarkcomplete}} {{/footer}} {{/ core/modal }} dataregistry.php 0000644 00000003546 15152701722 0007771 0 ustar 00 <?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/>. /** * Prints the data registry main page. * * @copyright 2018 onwards David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU Public License * @package tool_dataprivacy */ require_once(__DIR__ . '/../../../config.php'); require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php'); require_login(null, false); $contextlevel = optional_param('contextlevel', CONTEXT_SYSTEM, PARAM_INT); $contextid = optional_param('contextid', 0, PARAM_INT); $url = new moodle_url('/admin/tool/dataprivacy/dataregistry.php'); $title = get_string('dataregistry', 'tool_dataprivacy'); \tool_dataprivacy\page_helper::setup($url, $title); $output = $PAGE->get_renderer('tool_dataprivacy'); echo $output->header(); echo $OUTPUT->heading($title); if (\tool_dataprivacy\api::is_site_dpo($USER->id)) { $dataregistry = new tool_dataprivacy\output\data_registry_page($contextlevel, $contextid); echo $output->render($dataregistry); } else { $dponamestring = implode (', ', tool_dataprivacy\api::get_dpo_role_names()); $message = get_string('privacyofficeronly', 'tool_dataprivacy', $dponamestring); echo $OUTPUT->notification($message, 'error'); } echo $OUTPUT->footer(); createdatarequest_form.php 0000644 00000016323 15152701722 0012015 0 ustar 00 <?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/>. /** * The contact form to the site's Data Protection Officer * * @copyright 2018 onwards Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU Public License * @package tool_dataprivacy */ use tool_dataprivacy\api; use tool_dataprivacy\data_request; use tool_dataprivacy\local\helper; defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir.'/formslib.php'); /** * The contact form to the site's Data Protection Officer * * @copyright 2018 onwards Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU Public License * @package tool_dataprivacy */ class tool_dataprivacy_data_request_form extends \core\form\persistent { /** @var string Name of the persistent class. */ protected static $persistentclass = data_request::class; /** @var bool Flag to indicate whether this form is being rendered for managing data requests or for regular requests. */ protected $manage = false; /** * Form definition. * * @throws coding_exception * @throws dml_exception */ public function definition() { global $USER; $mform =& $this->_form; $this->manage = $this->_customdata['manage']; if ($this->manage) { $options = [ 'ajax' => 'tool_dataprivacy/form-user-selector', 'valuehtmlcallback' => function($value) { global $OUTPUT; $userfieldsapi = \core_user\fields::for_name(); $allusernames = $userfieldsapi->get_sql('', false, '', '', false)->selects; $fields = 'id, email, ' . $allusernames; $user = \core_user::get_user($value, $fields); $useroptiondata = [ 'fullname' => fullname($user), 'email' => $user->email ]; return $OUTPUT->render_from_template('tool_dataprivacy/form-user-selector-suggestion', $useroptiondata); } ]; $mform->addElement('autocomplete', 'userid', get_string('requestfor', 'tool_dataprivacy'), [], $options); $mform->addRule('userid', null, 'required', null, 'client'); } else { // Get users whom you are being a guardian to if your role has the capability to make data requests for children. if ($children = helper::get_children_of_user($USER->id)) { $useroptions = [ $USER->id => fullname($USER) ]; foreach ($children as $key => $child) { $useroptions[$key] = fullname($child); } $mform->addElement('autocomplete', 'userid', get_string('requestfor', 'tool_dataprivacy'), $useroptions); $mform->addRule('userid', null, 'required', null, 'client'); } else { // Requesting for self. $mform->addElement('hidden', 'userid', $USER->id); } } $mform->setType('userid', PARAM_INT); // Subject access request type. $options = []; if ($this->manage || api::can_create_data_download_request_for_self()) { $options[api::DATAREQUEST_TYPE_EXPORT] = get_string('requesttypeexport', 'tool_dataprivacy'); } $options[api::DATAREQUEST_TYPE_DELETE] = get_string('requesttypedelete', 'tool_dataprivacy'); $mform->addElement('select', 'type', get_string('requesttype', 'tool_dataprivacy'), $options); $mform->addHelpButton('type', 'requesttype', 'tool_dataprivacy'); // Request comments text area. $textareaoptions = ['cols' => 60, 'rows' => 10]; $mform->addElement('textarea', 'comments', get_string('requestcomments', 'tool_dataprivacy'), $textareaoptions); $mform->addHelpButton('comments', 'requestcomments', 'tool_dataprivacy'); // Action buttons. $this->add_action_buttons(); $shouldfreeze = false; if ($this->manage) { $shouldfreeze = !api::can_create_data_deletion_request_for_other(); } else { $shouldfreeze = !api::can_create_data_deletion_request_for_self(); if ($shouldfreeze && !empty($useroptions)) { foreach ($useroptions as $userid => $useroption) { if (api::can_create_data_deletion_request_for_children($userid)) { $shouldfreeze = false; break; } } } } if ($shouldfreeze) { $mform->freeze('type'); } } /** * Get the default data. Unset the default userid if managing data requests * * @return stdClass */ protected function get_default_data() { $data = parent::get_default_data(); if ($this->manage) { unset($data->userid); } return $data; } /** * Form validation. * * @param stdClass $data * @param array $files * @param array $errors * @return array * @throws coding_exception * @throws dml_exception */ public function extra_validation($data, $files, array &$errors) { global $USER; $validrequesttypes = [ api::DATAREQUEST_TYPE_EXPORT, api::DATAREQUEST_TYPE_DELETE ]; if (!in_array($data->type, $validrequesttypes)) { $errors['type'] = get_string('errorinvalidrequesttype', 'tool_dataprivacy'); } $userid = $data->userid; if (api::has_ongoing_request($userid, $data->type)) { $errors['type'] = get_string('errorrequestalreadyexists', 'tool_dataprivacy'); } // Check if current user can create data requests. if ($data->type == api::DATAREQUEST_TYPE_DELETE) { if ($userid == $USER->id) { if (!api::can_create_data_deletion_request_for_self()) { $errors['type'] = get_string('errorcannotrequestdeleteforself', 'tool_dataprivacy'); } } else if (!api::can_create_data_deletion_request_for_other() && !api::can_create_data_deletion_request_for_children($userid)) { $errors['type'] = get_string('errorcannotrequestdeleteforother', 'tool_dataprivacy'); } } else if ($data->type == api::DATAREQUEST_TYPE_EXPORT) { if ($userid == $USER->id && !api::can_create_data_download_request_for_self()) { $errors['type'] = get_string('errorcannotrequestexportforself', 'tool_dataprivacy'); } } return $errors; } } editpurpose.php 0000644 00000006010 15152701722 0007617 0 ustar 00 <?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/>. /** * This page lets users manage purposes. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once(__DIR__ . '/../../../config.php'); require_login(null, false); $id = optional_param('id', 0, PARAM_INT); $url = new \moodle_url('/admin/tool/dataprivacy/editpurpose.php', array('id' => $id)); if ($id) { $title = get_string('editpurpose', 'tool_dataprivacy'); } else { $title = get_string('addpurpose', 'tool_dataprivacy'); } \tool_dataprivacy\page_helper::setup($url, $title, 'dataregistry'); $purpose = new \tool_dataprivacy\purpose($id); $form = new \tool_dataprivacy\form\purpose($PAGE->url->out(false), array('persistent' => $purpose, 'showbuttons' => true)); $returnurl = new \moodle_url('/admin/tool/dataprivacy/purposes.php'); if ($form->is_cancelled()) { redirect($returnurl); } else if ($alldata = $form->get_data()) { $data = $form->filter_data_for_persistent($alldata); if (empty($data->id)) { $purpose = \tool_dataprivacy\api::create_purpose($data); $messagesuccess = get_string('purposecreated', 'tool_dataprivacy'); } else { $purpose = \tool_dataprivacy\api::update_purpose($data); $messagesuccess = get_string('purposeupdated', 'tool_dataprivacy'); } $currentoverrides = []; foreach ($purpose->get_purpose_overrides() as $override) { $currentoverrides[$override->get('id')] = $override; } $overrides = $form->get_role_overrides_from_data($alldata); $submittedoverrides = []; $tosave = []; foreach ($overrides as $overridedata) { $overridedata->purposeid = $purpose->get('id'); $override = new \tool_dataprivacy\purpose_override($overridedata->id, $overridedata); $tosave[] = $override; if (!empty($overridedata->id)) { $submittedoverrides[$overridedata->id] = true; } } foreach ($currentoverrides as $id => $override) { if (!isset($submittedoverrides[$id])) { $override->delete(); } } foreach ($tosave as $override) { $override->save(); } redirect($returnurl, $messagesuccess, 0, \core\output\notification::NOTIFY_SUCCESS); } $output = $PAGE->get_renderer('tool_dataprivacy'); echo $output->header(); $form->display(); echo $output->footer(); datadeletion.php 0000644 00000003675 15152701722 0007727 0 ustar 00 <?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/>. /** * Prints the data deletion main page. * * @copyright 2018 onwards Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU Public License * @package tool_dataprivacy */ require_once(__DIR__ . '/../../../config.php'); require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php'); require_login(null, false); $filter = optional_param('filter', CONTEXT_COURSE, PARAM_INT); $url = new moodle_url('/admin/tool/dataprivacy/datadeletion.php'); $title = get_string('datadeletion', 'tool_dataprivacy'); \tool_dataprivacy\page_helper::setup($url, $title); echo $OUTPUT->header(); echo $OUTPUT->heading($title); if (\tool_dataprivacy\api::is_site_dpo($USER->id)) { $table = new \tool_dataprivacy\output\expired_contexts_table($filter); $table->baseurl = $url; $table->baseurl->param('filter', $filter); $datadeletionpage = new \tool_dataprivacy\output\data_deletion_page($filter, $table); $output = $PAGE->get_renderer('tool_dataprivacy'); echo $output->render($datadeletionpage); } else { $dponamestring = implode (',', tool_dataprivacy\api::get_dpo_role_names()); $message = get_string('privacyofficeronly', 'tool_dataprivacy', $dponamestring); echo $OUTPUT->notification($message, 'error'); } echo $OUTPUT->footer(); settings.php 0000644 00000013500 15152701722 0007116 0 ustar 00 <?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/>. /** * Adds Data privacy-related settings. * * @package tool_dataprivacy * @copyright 2018 onwards Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; if ($hassiteconfig) { $privacysettings = $ADMIN->locate('privacysettings'); if ($ADMIN->fulltree) { // Contact data protection officer. Disabled by default. $privacysettings->add(new admin_setting_configcheckbox('tool_dataprivacy/contactdataprotectionofficer', new lang_string('contactdataprotectionofficer', 'tool_dataprivacy'), new lang_string('contactdataprotectionofficer_desc', 'tool_dataprivacy'), 0) ); $privacysettings->add(new admin_setting_configcheckbox('tool_dataprivacy/automaticdataexportapproval', new lang_string('automaticdataexportapproval', 'tool_dataprivacy'), new lang_string('automaticdataexportapproval_desc', 'tool_dataprivacy'), 0) ); $privacysettings->add(new admin_setting_configcheckbox('tool_dataprivacy/automaticdatadeletionapproval', new lang_string('automaticdatadeletionapproval', 'tool_dataprivacy'), new lang_string('automaticdatadeletionapproval_desc', 'tool_dataprivacy'), 0) ); // Automatically create delete data request for users upon user deletion. // Automatically create delete data request for pre-existing deleted users. // Enabled by default. $privacysettings->add(new admin_setting_configcheckbox('tool_dataprivacy/automaticdeletionrequests', new lang_string('automaticdeletionrequests', 'tool_dataprivacy'), new lang_string('automaticdeletionrequests_desc', 'tool_dataprivacy'), 1) ); // Set days approved data requests will be accessible. 1 week default. $privacysettings->add(new admin_setting_configduration('tool_dataprivacy/privacyrequestexpiry', new lang_string('privacyrequestexpiry', 'tool_dataprivacy'), new lang_string('privacyrequestexpiry_desc', 'tool_dataprivacy'), WEEKSECS, 1)); // Fetch roles that are assignable. $assignableroles = get_assignable_roles(context_system::instance()); // Fetch roles that have the capability to manage data requests. $capableroles = get_roles_with_capability('tool/dataprivacy:managedatarequests'); // Role(s) that map to the Data Protection Officer role. These are assignable roles with the capability to // manage data requests. $roles = []; foreach ($capableroles as $key => $role) { if (array_key_exists($key, $assignableroles)) { $roles[$key] = $assignableroles[$key]; } } if (!empty($roles)) { $privacysettings->add(new admin_setting_configmulticheckbox('tool_dataprivacy/dporoles', new lang_string('dporolemapping', 'tool_dataprivacy'), new lang_string('dporolemapping_desc', 'tool_dataprivacy'), null, $roles) ); } // When calculating user expiry, should courses which have no end date be considered. $privacysettings->add(new admin_setting_configcheckbox('tool_dataprivacy/requireallenddatesforuserdeletion', new lang_string('requireallenddatesforuserdeletion', 'tool_dataprivacy'), new lang_string('requireallenddatesforuserdeletion_desc', 'tool_dataprivacy'), 1)); // Whether the data retention summary should be shown in the page footer and in the user profile page. $privacysettings->add(new admin_setting_configcheckbox('tool_dataprivacy/showdataretentionsummary', new lang_string('showdataretentionsummary', 'tool_dataprivacy'), new lang_string('showdataretentionsummary_desc', 'tool_dataprivacy'), 1)); } } // Restrict config links to the DPO. if (tool_dataprivacy\api::is_site_dpo($USER->id)) { // Link that leads to the data requests management page. $ADMIN->add('privacy', new admin_externalpage('datarequests', get_string('datarequests', 'tool_dataprivacy'), new moodle_url('/admin/tool/dataprivacy/datarequests.php'), 'tool/dataprivacy:managedatarequests') ); // Link that leads to the data registry management page. $ADMIN->add('privacy', new admin_externalpage('dataregistry', get_string('dataregistry', 'tool_dataprivacy'), new moodle_url('/admin/tool/dataprivacy/dataregistry.php'), 'tool/dataprivacy:managedataregistry') ); // Link that leads to the review page of expired contexts that are up for deletion. $ADMIN->add('privacy', new admin_externalpage('datadeletion', get_string('datadeletion', 'tool_dataprivacy'), new moodle_url('/admin/tool/dataprivacy/datadeletion.php'), 'tool/dataprivacy:managedataregistry') ); // Link that leads to the other data registry management page. $ADMIN->add('privacy', new admin_externalpage('pluginregistry', get_string('pluginregistry', 'tool_dataprivacy'), new moodle_url('/admin/tool/dataprivacy/pluginregistry.php'), 'tool/dataprivacy:managedataregistry') ); } upgrade.txt 0000644 00000000442 15152701722 0006736 0 ustar 00 This file describes API changes in /admin/tool/dataprivacy/* Information provided here is intended especially for developers. === 4.1 === * New `api::can_create_data_download_request_for_self` method for determining whether user has permission to create their own data download requests mydatarequests.php 0000644 00000004071 15152701722 0010334 0 ustar 00 <?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/>. /** * Prints the contact form to the site's Data Protection Officer * * @copyright 2018 onwards Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU Public License * @package tool_dataprivacy */ require_once("../../../config.php"); require_once('lib.php'); $courseid = optional_param('course', 0, PARAM_INT); $url = new moodle_url('/admin/tool/dataprivacy/mydatarequests.php'); if ($courseid) { $url->param('course', $courseid); } $PAGE->set_url($url); require_login(); if (isguestuser()) { throw new \moodle_exception('noguest'); } $usercontext = context_user::instance($USER->id); $PAGE->set_context($usercontext); if ($profilenode = $PAGE->settingsnav->find('myprofile', null)) { $profilenode->make_active(); } $title = get_string('datarequests', 'tool_dataprivacy'); $PAGE->navbar->add($title); // Return URL. $params = ['id' => $USER->id]; if ($courseid) { $params['course'] = $courseid; } $returnurl = new moodle_url('/user/profile.php', $params); $PAGE->set_heading($title); $PAGE->set_title($title); echo $OUTPUT->header(); echo $OUTPUT->heading($title); $requests = tool_dataprivacy\api::get_data_requests($USER->id, [], [], [], 'timecreated DESC'); $requestlist = new tool_dataprivacy\output\my_data_requests_page($requests); $requestlistoutput = $PAGE->get_renderer('tool_dataprivacy'); echo $requestlistoutput->render($requestlist); echo $OUTPUT->footer(); datarequests.php 0000644 00000007415 15152701722 0007773 0 ustar 00 <?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/>. /** * Prints the contact form to the site's Data Protection Officer * * @copyright 2018 onwards Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU Public License * @package tool_dataprivacy */ require_once("../../../config.php"); require_once('lib.php'); require_login(null, false); $perpage = optional_param('perpage', 0, PARAM_INT); $url = new moodle_url('/admin/tool/dataprivacy/datarequests.php'); $title = get_string('datarequests', 'tool_dataprivacy'); \tool_dataprivacy\page_helper::setup($url, $title, '', 'tool/dataprivacy:managedatarequests'); echo $OUTPUT->header(); echo $OUTPUT->heading($title); if (!\tool_dataprivacy\data_registry::defaults_set()) { \core\notification::error(get_string('systemconfignotsetwarning', 'tool_dataprivacy')); } if (\tool_dataprivacy\api::is_site_dpo($USER->id)) { $filtersapplied = optional_param_array('request-filters', [-1], PARAM_NOTAGS); $filterscleared = optional_param('filters-cleared', 0, PARAM_INT); if ($filtersapplied === [-1]) { // If there are no filters submitted, check if there is a saved filters from the user preferences. $filterprefs = get_user_preferences(\tool_dataprivacy\local\helper::PREF_REQUEST_FILTERS, null); if ($filterprefs && empty($filterscleared)) { $filtersapplied = json_decode($filterprefs); } else { $filtersapplied = []; } } // Save the current applied filters to the user preferences. set_user_preference(\tool_dataprivacy\local\helper::PREF_REQUEST_FILTERS, json_encode($filtersapplied)); $types = []; $statuses = []; $creationmethods = []; foreach ($filtersapplied as $filter) { list($category, $value) = explode(':', $filter); switch($category) { case \tool_dataprivacy\local\helper::FILTER_TYPE: $types[] = $value; break; case \tool_dataprivacy\local\helper::FILTER_STATUS: $statuses[] = $value; break; case \tool_dataprivacy\local\helper::FILTER_CREATION: $creationmethods[] = $value; break; } } $table = new \tool_dataprivacy\output\data_requests_table(0, $statuses, $types, $creationmethods, true); if (!empty($perpage)) { set_user_preference(\tool_dataprivacy\local\helper::PREF_REQUEST_PERPAGE, $perpage); } else { $prefperpage = get_user_preferences(\tool_dataprivacy\local\helper::PREF_REQUEST_PERPAGE); $perpage = ($prefperpage) ? $prefperpage : $table->get_requests_per_page_options()[0]; } $table->set_requests_per_page($perpage); $table->baseurl = $url; $requestlist = new tool_dataprivacy\output\data_requests_page($table, $filtersapplied); $requestlistoutput = $PAGE->get_renderer('tool_dataprivacy'); echo $requestlistoutput->render($requestlist); } else { $dponamestring = implode (', ', tool_dataprivacy\api::get_dpo_role_names()); $message = get_string('privacyofficeronly', 'tool_dataprivacy', $dponamestring); echo $OUTPUT->notification($message, 'error'); } echo $OUTPUT->footer(); editcategory.php 0000644 00000004173 15152701722 0007747 0 ustar 00 <?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/>. /** * This page lets users manage categories. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once(__DIR__ . '/../../../config.php'); require_login(null, false); $id = optional_param('id', 0, PARAM_INT); $url = new \moodle_url('/admin/tool/dataprivacy/editcategory.php', array('id' => $id)); if ($id) { $title = get_string('editcategory', 'tool_dataprivacy'); } else { $title = get_string('addcategory', 'tool_dataprivacy'); } \tool_dataprivacy\page_helper::setup($url, $title, 'dataregistry'); $category = new \tool_dataprivacy\category($id); $form = new \tool_dataprivacy\form\category($PAGE->url->out(false), array('persistent' => $category, 'showbuttons' => true)); $returnurl = new \moodle_url('/admin/tool/dataprivacy/categories.php'); if ($form->is_cancelled()) { redirect($returnurl); } else if ($data = $form->get_data()) { if (empty($data->id)) { \tool_dataprivacy\api::create_category($data); $messagesuccess = get_string('categorycreated', 'tool_dataprivacy'); } else { \tool_dataprivacy\api::update_category($data); $messagesuccess = get_string('categoryupdated', 'tool_dataprivacy'); } redirect($returnurl, $messagesuccess, 0, \core\output\notification::NOTIFY_SUCCESS); } $output = $PAGE->get_renderer('tool_dataprivacy'); echo $output->header(); $form->display(); echo $output->footer(); lib.php 0000644 00000025405 15152701722 0006033 0 ustar 00 <?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/>. /** * Data privacy plugin library * @package tool_dataprivacy * @copyright 2018 onwards Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use core_user\output\myprofile\tree; defined('MOODLE_INTERNAL') || die(); /** * Add nodes to myprofile page. * * @param tree $tree Tree object * @param stdClass $user User object * @param bool $iscurrentuser * @param stdClass $course Course object * @return bool * @throws coding_exception * @throws dml_exception * @throws moodle_exception */ function tool_dataprivacy_myprofile_navigation(tree $tree, $user, $iscurrentuser, $course) { global $PAGE, $USER; // Get the Privacy and policies category. if (!array_key_exists('privacyandpolicies', $tree->__get('categories'))) { // Create the category. $categoryname = get_string('privacyandpolicies', 'admin'); $category = new core_user\output\myprofile\category('privacyandpolicies', $categoryname, 'contact'); $tree->add_category($category); } else { // Get the existing category. $category = $tree->__get('categories')['privacyandpolicies']; } // Contact data protection officer link. if (\tool_dataprivacy\api::can_contact_dpo() && $iscurrentuser) { $renderer = $PAGE->get_renderer('tool_dataprivacy'); $content = $renderer->render_contact_dpo_link(); $node = new core_user\output\myprofile\node('privacyandpolicies', 'contactdpo', null, null, null, $content); $category->add_node($node); // Require our Javascript module to handle contact DPO interaction. $PAGE->requires->js_call_amd('tool_dataprivacy/contactdpo', 'init'); $url = new moodle_url('/admin/tool/dataprivacy/mydatarequests.php'); $node = new core_user\output\myprofile\node('privacyandpolicies', 'datarequests', get_string('datarequests', 'tool_dataprivacy'), null, $url); $category->add_node($node); // Check if the user has an ongoing data export request. $hasexportrequest = \tool_dataprivacy\api::has_ongoing_request($user->id, \tool_dataprivacy\api::DATAREQUEST_TYPE_EXPORT); // Show data export link only if the user doesn't have an ongoing data export request and has permission // to download own data. if (!$hasexportrequest && \tool_dataprivacy\api::can_create_data_download_request_for_self()) { $exportparams = ['type' => \tool_dataprivacy\api::DATAREQUEST_TYPE_EXPORT]; $exporturl = new moodle_url('/admin/tool/dataprivacy/createdatarequest.php', $exportparams); $exportnode = new core_user\output\myprofile\node('privacyandpolicies', 'requestdataexport', get_string('requesttypeexport', 'tool_dataprivacy'), null, $exporturl); $category->add_node($exportnode); } // Check if the user has an ongoing data deletion request. $hasdeleterequest = \tool_dataprivacy\api::has_ongoing_request($user->id, \tool_dataprivacy\api::DATAREQUEST_TYPE_DELETE); // Show data deletion link only if the user doesn't have an ongoing data deletion request and has permission // to create data deletion request. if (!$hasdeleterequest && \tool_dataprivacy\api::can_create_data_deletion_request_for_self()) { $deleteparams = ['type' => \tool_dataprivacy\api::DATAREQUEST_TYPE_DELETE]; $deleteurl = new moodle_url('/admin/tool/dataprivacy/createdatarequest.php', $deleteparams); $deletenode = new core_user\output\myprofile\node('privacyandpolicies', 'requestdatadeletion', get_string('deletemyaccount', 'tool_dataprivacy'), null, $deleteurl); $category->add_node($deletenode); } } // A returned 0 means that the setting was set and disabled, false means that there is no value for the provided setting. $showsummary = get_config('tool_dataprivacy', 'showdataretentionsummary'); if ($showsummary === false) { // This means that no value is stored in db. We use the default value in this case. $showsummary = true; } if ($showsummary && $iscurrentuser) { $summaryurl = new moodle_url('/admin/tool/dataprivacy/summary.php'); $summarynode = new core_user\output\myprofile\node('privacyandpolicies', 'retentionsummary', get_string('dataretentionsummary', 'tool_dataprivacy'), null, $summaryurl); $category->add_node($summarynode); } // Add the Privacy category to the tree if it's not empty and it doesn't exist. $nodes = $category->nodes; if (!empty($nodes)) { if (!array_key_exists('privacyandpolicies', $tree->__get('categories'))) { $tree->add_category($category); } return true; } return false; } /** * Callback to add footer elements. * * @return string HTML footer content */ function tool_dataprivacy_standard_footer_html() { $output = ''; // A returned 0 means that the setting was set and disabled, false means that there is no value for the provided setting. $showsummary = get_config('tool_dataprivacy', 'showdataretentionsummary'); if ($showsummary === false) { // This means that no value is stored in db. We use the default value in this case. $showsummary = true; } if ($showsummary) { $url = new moodle_url('/admin/tool/dataprivacy/summary.php'); $output = html_writer::link($url, get_string('dataretentionsummary', 'tool_dataprivacy')); $output = html_writer::div($output, 'tool_dataprivacy'); } return $output; } /** * Fragment to add a new purpose. * * @param array $args The fragment arguments. * @return string The rendered mform fragment. */ function tool_dataprivacy_output_fragment_addpurpose_form($args) { $formdata = []; if (!empty($args['jsonformdata'])) { $serialiseddata = json_decode($args['jsonformdata']); parse_str($serialiseddata, $formdata); } $persistent = new \tool_dataprivacy\purpose(); $mform = new \tool_dataprivacy\form\purpose(null, ['persistent' => $persistent], 'post', '', null, true, $formdata); if (!empty($args['jsonformdata'])) { // Show errors if data was received. $mform->is_validated(); } return $mform->render(); } /** * Fragment to add a new category. * * @param array $args The fragment arguments. * @return string The rendered mform fragment. */ function tool_dataprivacy_output_fragment_addcategory_form($args) { $formdata = []; if (!empty($args['jsonformdata'])) { $serialiseddata = json_decode($args['jsonformdata']); parse_str($serialiseddata, $formdata); } $persistent = new \tool_dataprivacy\category(); $mform = new \tool_dataprivacy\form\category(null, ['persistent' => $persistent], 'post', '', null, true, $formdata); if (!empty($args['jsonformdata'])) { // Show errors if data was received. $mform->is_validated(); } return $mform->render(); } /** * Fragment to edit a context purpose and category. * * @param array $args The fragment arguments. * @return string The rendered mform fragment. */ function tool_dataprivacy_output_fragment_context_form($args) { global $PAGE; $contextid = $args[0]; $context = \context_helper::instance_by_id($contextid); $customdata = \tool_dataprivacy\form\context_instance::get_context_instance_customdata($context); if (!empty($customdata['purposeretentionperiods'])) { $PAGE->requires->js_call_amd('tool_dataprivacy/effective_retention_period', 'init', [$customdata['purposeretentionperiods']]); } $mform = new \tool_dataprivacy\form\context_instance(null, $customdata); return $mform->render(); } /** * Fragment to edit a contextlevel purpose and category. * * @param array $args The fragment arguments. * @return string The rendered mform fragment. */ function tool_dataprivacy_output_fragment_contextlevel_form($args) { global $PAGE; $contextlevel = $args[0]; $customdata = \tool_dataprivacy\form\contextlevel::get_contextlevel_customdata($contextlevel); if (!empty($customdata['purposeretentionperiods'])) { $PAGE->requires->js_call_amd('tool_dataprivacy/effective_retention_period', 'init', [$customdata['purposeretentionperiods']]); } $mform = new \tool_dataprivacy\form\contextlevel(null, $customdata); return $mform->render(); } /** * Serves any files associated with the data privacy settings. * * @param stdClass $course Course object * @param stdClass $cm Course module object * @param context $context Context * @param string $filearea File area for data privacy * @param array $args Arguments * @param bool $forcedownload If we are forcing the download * @param array $options More options * @return bool Returns false if we don't find a file. */ function tool_dataprivacy_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options = array()) { if ($context->contextlevel == CONTEXT_USER) { // Make sure the user is logged in. require_login(null, false); // Get the data request ID. This should be the first element of the $args array. $itemid = $args[0]; // Fetch the data request object. An invalid ID will throw an exception. $datarequest = new \tool_dataprivacy\data_request($itemid); // Check if user is allowed to download it. if (!\tool_dataprivacy\api::can_download_data_request_for_user($context->instanceid, $datarequest->get('requestedby'))) { return false; } // Make the file unavailable if it has expired. if (\tool_dataprivacy\data_request::is_expired($datarequest)) { send_file_not_found(); } // All good. Serve the exported data. $fs = get_file_storage(); $relativepath = implode('/', $args); $fullpath = "/$context->id/tool_dataprivacy/$filearea/$relativepath"; if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) { return false; } send_stored_file($file, 0, 0, $forcedownload, $options); } else { send_file_not_found(); } } version.php 0000644 00000002011 15152701722 0006736 0 ustar 00 <?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/>. /** * Data privacy plugin version information * * @package tool_dataprivacy * @copyright 2018 onwards Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; $plugin->version = 2022112800; $plugin->requires = 2022111800; $plugin->component = 'tool_dataprivacy'; classes/output/purposes.php 0000644 00000005425 15152701722 0012142 0 ustar 00 <?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/>. /** * Purposes renderable. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\output; defined('MOODLE_INTERNAL') || die(); use renderable; use renderer_base; use stdClass; use templatable; use tool_dataprivacy\external\purpose_exporter; /** * Class containing the purposes page renderable. * * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class purposes extends crud_element implements renderable, templatable { /** @var array $purposes All system purposes. */ protected $purposes = []; /** * Construct this renderable. * * @param \tool_dataprivacy\purpose[] $purposes */ public function __construct($purposes) { $this->purposes = $purposes; } /** * Export this data so it can be used as the context for a mustache template. * * @param renderer_base $output * @return stdClass */ public function export_for_template(renderer_base $output) { global $PAGE; $context = \context_system::instance(); $PAGE->requires->js_call_amd('tool_dataprivacy/purposesactions', 'init'); $PAGE->requires->js_call_amd('tool_dataprivacy/add_purpose', 'getInstance', [$context->id]); $data = new stdClass(); // Navigation links. $data->navigation = []; $navigationlinks = $this->get_navigation(); foreach ($navigationlinks as $navlink) { $data->navigation[] = $navlink->export_for_template($output); } $data->purposes = []; foreach ($this->purposes as $purpose) { $exporter = new purpose_exporter($purpose, ['context' => \context_system::instance()]); $exportedpurpose = $exporter->export($output); $actionmenu = $this->action_menu('purpose', $exportedpurpose, $purpose); $exportedpurpose->actions = $actionmenu->export_for_template($output); $data->purposes[] = $exportedpurpose; } return $data; } } classes/output/categories.php 0000644 00000005475 15152701722 0012414 0 ustar 00 <?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/>. /** * Categories renderable. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\output; defined('MOODLE_INTERNAL') || die(); use renderable; use renderer_base; use stdClass; use templatable; use tool_dataprivacy\external\category_exporter; /** * Class containing the categories page renderable. * * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class categories extends crud_element implements renderable, templatable { /** @var array $categories All system categories. */ protected $categories = []; /** * Construct this renderable. * * @param \tool_dataprivacy\category[] $categories */ public function __construct($categories) { $this->categories = $categories; } /** * Export this data so it can be used as the context for a mustache template. * * @param renderer_base $output * @return stdClass */ public function export_for_template(renderer_base $output) { global $PAGE; $context = \context_system::instance(); $PAGE->requires->js_call_amd('tool_dataprivacy/categoriesactions', 'init'); $PAGE->requires->js_call_amd('tool_dataprivacy/add_category', 'getInstance', [$context->id]); $data = new stdClass(); // Navigation links. $data->navigation = []; $navigationlinks = $this->get_navigation(); foreach ($navigationlinks as $navlink) { $data->navigation[] = $navlink->export_for_template($output); } $data->categories = []; foreach ($this->categories as $category) { $exporter = new category_exporter($category, ['context' => \context_system::instance()]); $exportedcategory = $exporter->export($output); $actionmenu = $this->action_menu('category', $exportedcategory, $category); $exportedcategory->actions = $actionmenu->export_for_template($output); $data->categories[] = $exportedcategory; } return $data; } } classes/output/renderer.php 0000644 00000011227 15152701722 0012065 0 ustar 00 <?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/>. /** * Renderer class for tool_dataprivacy * * @package tool_dataprivacy * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\output; defined('MOODLE_INTERNAL') || die(); use coding_exception; use html_writer; use moodle_exception; use plugin_renderer_base; /** * Renderer class for tool_dataprivacy. * * @package tool_dataprivacy * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class renderer extends plugin_renderer_base { /** * Render the user's data requests page. * * @param my_data_requests_page $page * @return string html for the page * @throws moodle_exception */ public function render_my_data_requests_page(my_data_requests_page $page) { $data = $page->export_for_template($this); return parent::render_from_template('tool_dataprivacy/my_data_requests', $data); } /** * Render the contact DPO link. * * @return string The HTML for the link. */ public function render_contact_dpo_link() { $params = [ 'data-action' => 'contactdpo', ]; return html_writer::link('#', get_string('contactdataprotectionofficer', 'tool_dataprivacy'), $params); } /** * Render the data requests page for the DPO. * * @param data_requests_page $page * @return string html for the page * @throws moodle_exception */ public function render_data_requests_page(data_requests_page $page) { $data = $page->export_for_template($this); return parent::render_from_template('tool_dataprivacy/data_requests', $data); } /** * Render the data registry. * * @param data_registry_page $page * @return string html for the page * @throws moodle_exception */ public function render_data_registry_page(data_registry_page $page) { $data = $page->export_for_template($this); return parent::render_from_template('tool_dataprivacy/data_registry', $data); } /** * Render the data compliance registry. * * @param data_registry_compliance_page $page * @return string html for the page * @throws moodle_exception */ public function render_data_registry_compliance_page(data_registry_compliance_page $page) { $data = $page->export_for_template($this); return parent::render_from_template('tool_dataprivacy/data_registry_compliance', $data); } /** * Render the purposes management page. * * @param purposes $page * @return string html for the page * @throws moodle_exception */ public function render_purposes(purposes $page) { $data = $page->export_for_template($this); return parent::render_from_template('tool_dataprivacy/purposes', $data); } /** * Render the categories management page. * * @param categories $page * @return string html for the page * @throws moodle_exception */ public function render_categories(categories $page) { $data = $page->export_for_template($this); return parent::render_from_template('tool_dataprivacy/categories', $data); } /** * Render the review page for the deletion of expired contexts. * * @param data_deletion_page $page * @return string html for the page * @throws moodle_exception */ public function render_data_deletion_page(data_deletion_page $page) { $data = $page->export_for_template($this); return parent::render_from_template('tool_dataprivacy/data_deletion', $data); } /** * Render the user data retention summary page. * * @param summary_page $page * @return string html for the page. */ public function render_summary_page(summary_page $page) { $data = $page->export_for_template($this); return parent::render_from_template('tool_dataprivacy/summary', $data); } } classes/output/data_requests_table.php 0000644 00000037354 15152701722 0014303 0 ustar 00 <?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/>. /** * Contains the class used for the displaying the data requests table. * * @package tool_dataprivacy * @copyright 2018 Jun Pataleta <jun@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\output; defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir . '/tablelib.php'); use action_menu; use action_menu_link_secondary; use coding_exception; use dml_exception; use html_writer; use moodle_url; use stdClass; use table_sql; use tool_dataprivacy\api; use tool_dataprivacy\external\data_request_exporter; defined('MOODLE_INTERNAL') || die; /** * The class for displaying the data requests table. * * @copyright 2018 Jun Pataleta <jun@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class data_requests_table extends table_sql { /** @var int The user ID. */ protected $userid = 0; /** @var int[] The status filters. */ protected $statuses = []; /** @var int[] The request type filters. */ protected $types = []; /** @var bool Whether this table is being rendered for managing data requests. */ protected $manage = false; /** @var \tool_dataprivacy\data_request[] Array of data request persistents. */ protected $datarequests = []; /** @var \stdClass[] List of userids and whether they have any ongoing active requests. */ protected $ongoingrequests = []; /** @var int The number of data request to be displayed per page. */ protected $perpage; /** @var int[] The available options for the number of data request to be displayed per page. */ protected $perpageoptions = [25, 50, 100, 250]; /** * data_requests_table constructor. * * @param int $userid The user ID * @param int[] $statuses * @param int[] $types * @param int[] $creationmethods * @param bool $manage * @throws coding_exception */ public function __construct($userid = 0, $statuses = [], $types = [], $creationmethods = [], $manage = false) { parent::__construct('data-requests-table'); $this->userid = $userid; $this->statuses = $statuses; $this->types = $types; $this->creationmethods = $creationmethods; $this->manage = $manage; $checkboxattrs = [ 'title' => get_string('selectall'), 'data-action' => 'selectall' ]; $columnheaders = [ 'select' => html_writer::checkbox('selectall', 1, false, null, $checkboxattrs), 'type' => get_string('requesttype', 'tool_dataprivacy'), 'userid' => get_string('user', 'tool_dataprivacy'), 'timecreated' => get_string('daterequested', 'tool_dataprivacy'), 'requestedby' => get_string('requestby', 'tool_dataprivacy'), 'status' => get_string('requeststatus', 'tool_dataprivacy'), 'comments' => get_string('message', 'tool_dataprivacy'), 'actions' => '', ]; $this->define_columns(array_keys($columnheaders)); $this->define_headers(array_values($columnheaders)); $this->no_sorting('select', 'actions'); } /** * The select column. * * @param stdClass $data The row data. * @return string * @throws \moodle_exception * @throws coding_exception */ public function col_select($data) { if ($data->status == \tool_dataprivacy\api::DATAREQUEST_STATUS_AWAITING_APPROVAL) { if ($data->type == \tool_dataprivacy\api::DATAREQUEST_TYPE_DELETE && !api::can_create_data_deletion_request_for_other()) { // Don't show checkbox if request's type is delete and user don't have permission. return false; } $stringdata = [ 'username' => $data->foruser->fullname, 'requesttype' => \core_text::strtolower($data->typenameshort) ]; return \html_writer::checkbox('requestids[]', $data->id, false, '', ['class' => 'selectrequests', 'title' => get_string('selectuserdatarequest', 'tool_dataprivacy', $stringdata)]); } } /** * The type column. * * @param stdClass $data The row data. * @return string */ public function col_type($data) { if ($this->manage) { return $data->typenameshort; } return $data->typename; } /** * The user column. * * @param stdClass $data The row data. * @return mixed */ public function col_userid($data) { $user = $data->foruser; return html_writer::link($user->profileurl, $user->fullname, ['title' => get_string('viewprofile')]); } /** * The context information column. * * @param stdClass $data The row data. * @return string */ public function col_timecreated($data) { return userdate($data->timecreated); } /** * The requesting user's column. * * @param stdClass $data The row data. * @return mixed */ public function col_requestedby($data) { $user = $data->requestedbyuser; return html_writer::link($user->profileurl, $user->fullname, ['title' => get_string('viewprofile')]); } /** * The status column. * * @param stdClass $data The row data. * @return mixed */ public function col_status($data) { return html_writer::span($data->statuslabel, 'badge ' . $data->statuslabelclass); } /** * The comments column. * * @param stdClass $data The row data. * @return string */ public function col_comments($data) { return shorten_text($data->comments, 60); } /** * The actions column. * * @param stdClass $data The row data. * @return string */ public function col_actions($data) { global $OUTPUT; $requestid = $data->id; $status = $data->status; $persistent = $this->datarequests[$requestid]; // Prepare actions. $actions = []; // View action. $actionurl = new moodle_url('#'); $actiondata = ['data-action' => 'view', 'data-requestid' => $requestid]; $actiontext = get_string('viewrequest', 'tool_dataprivacy'); $actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata); switch ($status) { case api::DATAREQUEST_STATUS_PENDING: // Add action to mark a general enquiry request as complete. if ($data->type == api::DATAREQUEST_TYPE_OTHERS) { $actiondata['data-action'] = 'complete'; $nameemail = (object)[ 'name' => $data->foruser->fullname, 'email' => $data->foruser->email ]; $actiondata['data-requestid'] = $data->id; $actiondata['data-replytoemail'] = get_string('nameemail', 'tool_dataprivacy', $nameemail); $actiontext = get_string('markcomplete', 'tool_dataprivacy'); $actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata); } break; case api::DATAREQUEST_STATUS_AWAITING_APPROVAL: // Only show "Approve" and "Deny" button for deletion request if current user has permission. if ($persistent->get('type') == api::DATAREQUEST_TYPE_DELETE && !api::can_create_data_deletion_request_for_other()) { break; } // Approve. $actiondata['data-action'] = 'approve'; $actiontext = get_string('approverequest', 'tool_dataprivacy'); $actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata); // Deny. $actiondata['data-action'] = 'deny'; $actiontext = get_string('denyrequest', 'tool_dataprivacy'); $actions[] = new action_menu_link_secondary($actionurl, null, $actiontext, $actiondata); break; case api::DATAREQUEST_STATUS_DOWNLOAD_READY: $userid = $data->foruser->id; $usercontext = \context_user::instance($userid, IGNORE_MISSING); // If user has permission to view download link, show relevant action item. if ($usercontext && api::can_download_data_request_for_user($userid, $data->requestedbyuser->id)) { $actions[] = api::get_download_link($usercontext, $requestid); } break; } if ($this->manage) { $canreset = $persistent->is_active() || empty($this->ongoingrequests[$data->foruser->id]->{$data->type}); $canreset = $canreset && $persistent->is_resettable(); // Prevent re-submmit deletion request if current user don't have permission. $canreset = $canreset && ($persistent->get('type') != api::DATAREQUEST_TYPE_DELETE || api::can_create_data_deletion_request_for_other()); if ($canreset) { $reseturl = new moodle_url('/admin/tool/dataprivacy/resubmitrequest.php', [ 'requestid' => $requestid, ]); $actiondata = ['data-action' => 'reset', 'data-requestid' => $requestid]; $actiontext = get_string('resubmitrequestasnew', 'tool_dataprivacy'); $actions[] = new action_menu_link_secondary($reseturl, null, $actiontext, $actiondata); } } $actionsmenu = new action_menu($actions); $actionsmenu->set_menu_trigger(get_string('actions')); $actionsmenu->set_owner_selector('request-actions-' . $requestid); $actionsmenu->set_constraint('[data-region=data-requests-table] > .no-overflow'); return $OUTPUT->render($actionsmenu); } /** * Query the database for results to display in the table. * * @param int $pagesize size of page for paginated displayed table. * @param bool $useinitialsbar do you want to use the initials bar. * @throws dml_exception * @throws coding_exception */ public function query_db($pagesize, $useinitialsbar = true) { global $PAGE; // Set dummy page total until we fetch full result set. $this->pagesize($pagesize, $pagesize + 1); $sort = $this->get_sql_sort(); // Get data requests from the given conditions. $datarequests = api::get_data_requests($this->userid, $this->statuses, $this->types, $this->creationmethods, $sort, $this->get_page_start(), $this->get_page_size()); // Count data requests from the given conditions. $total = api::get_data_requests_count($this->userid, $this->statuses, $this->types, $this->creationmethods); $this->pagesize($pagesize, $total); $this->rawdata = []; $context = \context_system::instance(); $renderer = $PAGE->get_renderer('tool_dataprivacy'); $forusers = []; foreach ($datarequests as $persistent) { $this->datarequests[$persistent->get('id')] = $persistent; $exporter = new data_request_exporter($persistent, ['context' => $context]); $this->rawdata[] = $exporter->export($renderer); $forusers[] = $persistent->get('userid'); } // Fetch the list of all ongoing requests for the users currently shown. // This is used to determine whether any non-active request can be resubmitted. // There can only be one ongoing request of a type for each user. $this->ongoingrequests = api::find_ongoing_request_types_for_users($forusers); // Set initial bars. if ($useinitialsbar) { $this->initialbars($total > $pagesize); } } /** * Override default implementation to display a more meaningful information to the user. */ public function print_nothing_to_display() { global $OUTPUT; echo $this->render_reset_button(); $this->print_initials_bar(); if (!empty($this->statuses) || !empty($this->types)) { $message = get_string('nodatarequestsmatchingfilter', 'tool_dataprivacy'); } else { $message = get_string('nodatarequests', 'tool_dataprivacy'); } echo $OUTPUT->notification($message, 'warning'); } /** * Override the table's show_hide_link method to prevent the show/hide links from rendering. * * @param string $column the column name, index into various names. * @param int $index numerical index of the column. * @return string HTML fragment. */ protected function show_hide_link($column, $index) { return ''; } /** * Override the table's wrap_html_finish method in order to render the bulk actions and * records per page options. */ public function wrap_html_finish() { global $OUTPUT; $data = new stdClass(); $data->options = [ [ 'value' => 0, 'name' => '' ], [ 'value' => \tool_dataprivacy\api::DATAREQUEST_ACTION_APPROVE, 'name' => get_string('approve', 'tool_dataprivacy') ], [ 'value' => \tool_dataprivacy\api::DATAREQUEST_ACTION_REJECT, 'name' => get_string('deny', 'tool_dataprivacy') ] ]; $perpageoptions = array_combine($this->perpageoptions, $this->perpageoptions); $perpageselect = new \single_select(new moodle_url(''), 'perpage', $perpageoptions, get_user_preferences('tool_dataprivacy_request-perpage'), null, 'selectgroup'); $perpageselect->label = get_string('perpage', 'moodle'); $data->perpage = $OUTPUT->render($perpageselect); echo $OUTPUT->render_from_template('tool_dataprivacy/data_requests_bulk_actions', $data); } /** * Set the number of data request records to be displayed per page. * * @param int $perpage The number of data request records. */ public function set_requests_per_page(int $perpage) { $this->perpage = $perpage; } /** * Get the number of data request records to be displayed per page. * * @return int The number of data request records. */ public function get_requests_per_page() : int { return $this->perpage; } /** * Set the available options for the number of data request to be displayed per page. * * @param array $perpageoptions The available options for the number of data request to be displayed per page. */ public function set_requests_per_page_options(array $perpageoptions) { $this->$perpageoptions = $perpageoptions; } /** * Get the available options for the number of data request to be displayed per page. * * @return array The available options for the number of data request to be displayed per page. */ public function get_requests_per_page_options() : array { return $this->perpageoptions; } } classes/output/data_deletion_page.php 0000644 00000006113 15152701722 0014045 0 ustar 00 <?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/>. /** * Class containing data for a user's data requests. * * @package tool_dataprivacy * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\output; defined('MOODLE_INTERNAL') || die(); use coding_exception; use moodle_exception; use moodle_url; use renderable; use renderer_base; use single_select; use stdClass; use templatable; use tool_dataprivacy\data_request; use tool_dataprivacy\local\helper; /** * Class containing data for a user's data requests. * * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class data_deletion_page implements renderable, templatable { /** @var data_request[] $requests List of data requests. */ protected $filter = null; /** @var data_request[] $requests List of data requests. */ protected $expiredcontextstable = []; /** * Construct this renderable. * * @param \tool_dataprivacy\data_request[] $filter * @param expired_contexts_table $expiredcontextstable */ public function __construct($filter, expired_contexts_table $expiredcontextstable) { $this->filter = $filter; $this->expiredcontextstable = $expiredcontextstable; } /** * Export this data so it can be used as the context for a mustache template. * * @param renderer_base $output * @return stdClass * @throws coding_exception * @throws moodle_exception */ public function export_for_template(renderer_base $output) { $data = new stdClass(); $url = new moodle_url('/admin/tool/dataprivacy/datadeletion.php'); $options = [ CONTEXT_USER => get_string('user'), CONTEXT_COURSE => get_string('course'), CONTEXT_MODULE => get_string('activitiesandresources', 'tool_dataprivacy'), CONTEXT_BLOCK => get_string('blocks'), ]; $filterselector = new single_select($url, 'filter', $options, $this->filter, null); $data->filter = $filterselector->export_for_template($output); ob_start(); $this->expiredcontextstable->out(helper::DEFAULT_PAGE_SIZE, true); $expiredcontexts = ob_get_contents(); ob_end_clean(); $data->expiredcontexts = $expiredcontexts; $data->existingcontexts = $this->expiredcontextstable->rawdata ? true : false; return $data; } } classes/output/data_requests_page.php 0000644 00000006056 15152701722 0014123 0 ustar 00 <?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/>. /** * Class containing data for a user's data requests. * * @package tool_dataprivacy * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\output; defined('MOODLE_INTERNAL') || die(); use coding_exception; use dml_exception; use moodle_exception; use moodle_url; use renderable; use renderer_base; use single_select; use stdClass; use templatable; use tool_dataprivacy\api; use tool_dataprivacy\local\helper; /** * Class containing data for a user's data requests. * * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class data_requests_page implements renderable, templatable { /** @var data_requests_table $table The data requests table. */ protected $table; /** @var int[] $filters The applied filters. */ protected $filters = []; /** * Construct this renderable. * * @param data_requests_table $table The data requests table. * @param int[] $filters The applied filters. */ public function __construct($table, $filters) { $this->table = $table; $this->filters = $filters; } /** * Export this data so it can be used as the context for a mustache template. * * @param renderer_base $output * @return stdClass * @throws coding_exception * @throws dml_exception * @throws moodle_exception */ public function export_for_template(renderer_base $output) { $data = new stdClass(); $data->newdatarequesturl = new moodle_url('/admin/tool/dataprivacy/createdatarequest.php'); $data->newdatarequesturl->param('manage', true); if (!is_https()) { $httpwarningmessage = get_string('httpwarning', 'tool_dataprivacy'); $data->httpsite = array('message' => $httpwarningmessage, 'announce' => 1); } $url = new moodle_url('/admin/tool/dataprivacy/datarequests.php'); $filteroptions = helper::get_request_filter_options(); $filter = new request_filter($filteroptions, $this->filters, $url); $data->filter = $filter->export_for_template($output); ob_start(); $this->table->out($this->table->get_requests_per_page(), true); $requests = ob_get_contents(); ob_end_clean(); $data->datarequests = $requests; return $data; } } classes/output/defaults_page.php 0000644 00000014736 15152701722 0013072 0 ustar 00 <?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/>. /** * Class containing data for the data registry defaults. * * @package tool_dataprivacy * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\output; defined('MOODLE_INTERNAL') || die(); use action_menu_link_primary; use coding_exception; use moodle_exception; use moodle_url; use renderable; use renderer_base; use stdClass; use templatable; use tool_dataprivacy\data_registry; use tool_dataprivacy\external\category_exporter; use tool_dataprivacy\external\purpose_exporter; /** * Class containing data for the data registry defaults. * * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class defaults_page implements renderable, templatable { /** @var int $mode The display mode. */ protected $mode = null; /** @var int $category The default category for the given mode. */ protected $category = null; /** @var int $purpose The default purpose for the given mode. */ protected $purpose = null; /** @var stdClass[] $otherdefaults Other defaults for the given mode. */ protected $otherdefaults = []; /** @var bool $canedit Whether editing is allowed. */ protected $canedit = false; /** * Construct this renderable. * * @param int $mode The display mode. * @param int $category The default category for the given mode. * @param int $purpose The default purpose for the given mode. * @param stdClass[] $otherdefaults Other defaults for the given mode. * @param bool $canedit Whether editing is allowed. */ public function __construct($mode, $category, $purpose, $otherdefaults = [], $canedit = false) { $this->mode = $mode; $this->category = $category; $this->purpose = $purpose; $this->otherdefaults = $otherdefaults; $this->canedit = $canedit; } /** * Export this data so it can be used as the context for a mustache template. * * @param renderer_base $output * @return stdClass * @throws coding_exception * @throws moodle_exception */ public function export_for_template(renderer_base $output) { $data = new stdClass(); // Set tab URLs. $coursecaturl = new moodle_url('/admin/tool/dataprivacy/defaults.php', ['mode' => CONTEXT_COURSECAT]); $courseurl = new moodle_url('/admin/tool/dataprivacy/defaults.php', ['mode' => CONTEXT_COURSE]); $moduleurl = new moodle_url('/admin/tool/dataprivacy/defaults.php', ['mode' => CONTEXT_MODULE]); $blockurl = new moodle_url('/admin/tool/dataprivacy/defaults.php', ['mode' => CONTEXT_BLOCK]); $data->coursecaturl = $coursecaturl; $data->courseurl = $courseurl; $data->moduleurl = $moduleurl; $data->blockurl = $blockurl; // Set display mode. switch ($this->mode) { case CONTEXT_COURSECAT: $data->modecoursecat = true; break; case CONTEXT_COURSE: $data->modecourse = true; break; case CONTEXT_MODULE: $data->modemodule = true; break; case CONTEXT_BLOCK: $data->modeblock = true; break; default: $data->modecoursecat = true; break; } // Set config variables. $configname = \context_helper::get_class_for_level($this->mode); list($purposevar, $categoryvar) = data_registry::var_names_from_context($configname); $data->categoryvar = $categoryvar; $data->purposevar = $purposevar; // Set default category. $data->categoryid = $this->category; $data->category = category_exporter::get_name($this->category); // Set default purpose. $data->purposeid = $this->purpose; $data->purpose = purpose_exporter::get_name($this->purpose); // Set other defaults. $otherdefaults = []; $url = new moodle_url('#'); foreach ($this->otherdefaults as $pluginname => $values) { $defaults = [ 'name' => $values->name, 'category' => category_exporter::get_name($values->category), 'purpose' => purpose_exporter::get_name($values->purpose), ]; if ($this->canedit) { $actions = []; // Edit link. $editattrs = [ 'data-action' => 'edit-activity-defaults', 'data-contextlevel' => $this->mode, 'data-activityname' => $pluginname, 'data-category' => $values->category, 'data-purpose' => $values->purpose, ]; $editlink = new action_menu_link_primary($url, new \pix_icon('t/edit', get_string('edit')), get_string('edit'), $editattrs); $actions[] = $editlink->export_for_template($output); // Delete link. $deleteattrs = [ 'data-action' => 'delete-activity-defaults', 'data-contextlevel' => $this->mode, 'data-activityname' => $pluginname, 'data-activitydisplayname' => $values->name, ]; $deletelink = new action_menu_link_primary($url, new \pix_icon('t/delete', get_string('delete')), get_string('delete'), $deleteattrs); $actions[] = $deletelink->export_for_template($output); $defaults['actions'] = $actions; } $otherdefaults[] = (object)$defaults; } $data->otherdefaults = $otherdefaults; $data->canedit = $this->canedit; $data->contextlevel = $this->mode; return $data; } } classes/output/request_filter.php 0000644 00000006544 15152701722 0013322 0 ustar 00 <?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/>. /** * Class containing the filter options data for rendering the autocomplete element for the data requests page. * * @package tool_dataprivacy * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\output; use moodle_url; use renderable; use renderer_base; use stdClass; use templatable; defined('MOODLE_INTERNAL') || die(); /** * Class containing the filter options data for rendering the autocomplete element for the data requests page. * * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class request_filter implements renderable, templatable { /** @var array $filteroptions The filter options. */ protected $filteroptions; /** @var array $selectedoptions The list of selected filter option values. */ protected $selectedoptions; /** @var moodle_url|string $baseurl The url with params needed to call up this page. */ protected $baseurl; /** * request_filter constructor. * * @param array $filteroptions The filter options. * @param array $selectedoptions The list of selected filter option values. * @param string|moodle_url $baseurl The url with params needed to call up this page. */ public function __construct($filteroptions, $selectedoptions, $baseurl = null) { $this->filteroptions = $filteroptions; $this->selectedoptions = $selectedoptions; if (!empty($baseurl)) { $this->baseurl = new moodle_url($baseurl); } } /** * Function to export the renderer data in a format that is suitable for a mustache template. * * @param renderer_base $output Used to do a final render of any components that need to be rendered for export. * @return stdClass|array */ public function export_for_template(renderer_base $output) { global $PAGE; $data = new stdClass(); if (empty($this->baseurl)) { $this->baseurl = $PAGE->url; } $data->action = $this->baseurl->out(false); foreach ($this->selectedoptions as $option) { if (!isset($this->filteroptions[$option])) { $this->filteroptions[$option] = $option; } } $data->filteroptions = []; foreach ($this->filteroptions as $value => $label) { $selected = in_array($value, $this->selectedoptions); $filteroption = (object)[ 'value' => $value, 'label' => $label ]; $filteroption->selected = $selected; $data->filteroptions[] = $filteroption; } return $data; } } classes/output/my_data_requests_page.php 0000644 00000014205 15152701722 0014623 0 ustar 00 <?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/>. /** * Class containing data for a user's data requests. * * @package tool_dataprivacy * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\output; defined('MOODLE_INTERNAL') || die(); use action_menu; use action_menu_link_secondary; use coding_exception; use context_user; use moodle_exception; use moodle_url; use renderable; use renderer_base; use stdClass; use templatable; use tool_dataprivacy\api; use tool_dataprivacy\data_request; use tool_dataprivacy\external\data_request_exporter; /** * Class containing data for a user's data requests. * * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class my_data_requests_page implements renderable, templatable { /** @var array $requests List of data requests. */ protected $requests = []; /** * Construct this renderable. * * @param data_request[] $requests */ public function __construct($requests) { $this->requests = $requests; } /** * Export this data so it can be used as the context for a mustache template. * * @param renderer_base $output * @return stdClass * @throws coding_exception * @throws moodle_exception */ public function export_for_template(renderer_base $output) { global $USER; $data = new stdClass(); $data->newdatarequesturl = new moodle_url('/admin/tool/dataprivacy/createdatarequest.php'); if (!is_https()) { $httpwarningmessage = get_string('httpwarning', 'tool_dataprivacy'); $data->httpsite = array('message' => $httpwarningmessage, 'announce' => 1); } $requests = []; foreach ($this->requests as $request) { $requestid = $request->get('id'); $status = $request->get('status'); $userid = $request->get('userid'); $type = $request->get('type'); $usercontext = context_user::instance($userid, IGNORE_MISSING); if (!$usercontext) { // Use the context system. $outputcontext = \context_system::instance(); } else { $outputcontext = $usercontext; } $requestexporter = new data_request_exporter($request, ['context' => $outputcontext]); $item = $requestexporter->export($output); $self = $request->get('userid') == $USER->id; if (!$self) { // Append user name if it differs from $USER. $a = (object)['typename' => $item->typename, 'user' => $item->foruser->fullname]; $item->typename = get_string('requesttypeuser', 'tool_dataprivacy', $a); } $candownload = false; $cancancel = true; switch ($status) { case api::DATAREQUEST_STATUS_COMPLETE: $item->statuslabelclass = 'badge-success'; $item->statuslabel = get_string('statuscomplete', 'tool_dataprivacy'); $cancancel = false; break; case api::DATAREQUEST_STATUS_DOWNLOAD_READY: $item->statuslabelclass = 'badge-success'; $item->statuslabel = get_string('statusready', 'tool_dataprivacy'); $cancancel = false; $candownload = true; if ($usercontext) { $candownload = api::can_download_data_request_for_user( $request->get('userid'), $request->get('requestedby')); } break; case api::DATAREQUEST_STATUS_DELETED: $item->statuslabelclass = 'badge-success'; $item->statuslabel = get_string('statusdeleted', 'tool_dataprivacy'); $cancancel = false; break; case api::DATAREQUEST_STATUS_EXPIRED: $item->statuslabelclass = 'badge-secondary'; $item->statuslabel = get_string('statusexpired', 'tool_dataprivacy'); $item->statuslabeltitle = get_string('downloadexpireduser', 'tool_dataprivacy'); $cancancel = false; break; case api::DATAREQUEST_STATUS_CANCELLED: case api::DATAREQUEST_STATUS_REJECTED: $cancancel = false; break; } // Prepare actions. $actions = []; if ($cancancel) { $cancelurl = new moodle_url('#'); $canceldata = ['data-action' => 'cancel', 'data-requestid' => $requestid]; $canceltext = get_string('cancelrequest', 'tool_dataprivacy'); $actions[] = new action_menu_link_secondary($cancelurl, null, $canceltext, $canceldata); } if ($candownload && $usercontext) { $actions[] = api::get_download_link($usercontext, $requestid); } if (!empty($actions)) { $actionsmenu = new action_menu($actions); $actionsmenu->set_menu_trigger(get_string('actions')); $actionsmenu->set_owner_selector('request-actions-' . $requestid); $item->actions = $actionsmenu->export_for_template($output); } $requests[] = $item; } $data->requests = $requests; return $data; } } classes/output/crud_element.php 0000644 00000006055 15152701722 0012730 0 ustar 00 <?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/>. /** * Abstract renderer for independent renderable elements. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\output; defined('MOODLE_INTERNAL') || die(); use renderable; use renderer_base; use stdClass; use templatable; use tool_dataprivacy\external\purpose_exporter; use tool_dataprivacy\external\category_exporter; /** * Abstract renderer for independent renderable elements. * * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class crud_element { /** * Returns the top navigation buttons. * * @return \action_link[] */ protected final function get_navigation() { $back = new \action_link( new \moodle_url('/admin/tool/dataprivacy/dataregistry.php'), get_string('back'), null, ['class' => 'btn btn-primary'] ); return [$back]; } /** * Adds an action menu for the provided element * * @param string $elementname 'purpose' or 'category'. * @param \stdClass $exported * @param \core\persistent $persistent * @return \action_menu */ protected final function action_menu($elementname, $exported, $persistent) { // Just in case, we are doing funny stuff below. $elementname = clean_param($elementname, PARAM_ALPHA); // Actions. $actionmenu = new \action_menu(); $actionmenu->set_menu_trigger(get_string('actions')); $actionmenu->set_owner_selector($elementname . '-' . $exported->id . '-actions'); $url = new \moodle_url('/admin/tool/dataprivacy/edit' . $elementname . '.php', ['id' => $exported->id]); $link = new \action_menu_link_secondary($url, new \pix_icon('t/edit', get_string('edit')), get_string('edit')); $actionmenu->add($link); if (!$persistent->is_used()) { $url = new \moodle_url('#'); $attrs = ['data-id' => $exported->id, 'data-action' => 'delete' . $elementname, 'data-name' => $exported->name]; $link = new \action_menu_link_secondary($url, new \pix_icon('t/delete', get_string('delete')), get_string('delete'), $attrs); $actionmenu->add($link); } return $actionmenu; } } classes/output/summary_page.php 0000644 00000012267 15152701722 0012755 0 ustar 00 <?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/>. /** * Summary page renderable. * * @package tool_dataprivacy * @copyright 2018 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\output; defined('MOODLE_INTERNAL') || die(); use renderable; use renderer_base; use templatable; /** * Class containing the summary page renderable. * * @copyright 2018 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class summary_page implements renderable, templatable { /** * Export this data so it can be used as the context for a mustache template. * * @param renderer_base $output * @return array */ public function export_for_template(renderer_base $output) { $contextlevels = [ 'contextlevelname10' => CONTEXT_SYSTEM, 'contextlevelname30' => CONTEXT_USER, 'contextlevelname40' => CONTEXT_COURSECAT, 'contextlevelname50' => CONTEXT_COURSE, 'contextlevelname70' => CONTEXT_MODULE, 'contextlevelname80' => CONTEXT_BLOCK ]; $data = []; $context = \context_system::instance(); foreach ($contextlevels as $levelname => $level) { $classname = \context_helper::get_class_for_level($level); list($purposevar, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname); $purposeid = get_config('tool_dataprivacy', $purposevar); $categoryid = get_config('tool_dataprivacy', $categoryvar); $section = []; $section['contextname'] = get_string($levelname, 'tool_dataprivacy'); if (empty($purposeid)) { list($purposeid, $categoryid) = \tool_dataprivacy\data_registry::get_effective_default_contextlevel_purpose_and_category($level); } if ($purposeid == -1) { $purposeid = 0; } $purpose = new \tool_dataprivacy\purpose($purposeid); $export = new \tool_dataprivacy\external\purpose_exporter($purpose, ['context' => $context]); $purposedata = $export->export($output); $section['purpose'] = $purposedata; if (empty($categoryid)) { list($purposeid, $categoryid) = \tool_dataprivacy\data_registry::get_effective_default_contextlevel_purpose_and_category($level); } if ($categoryid == -1) { $categoryid = 0; } $category = new \tool_dataprivacy\category($categoryid); $export = new \tool_dataprivacy\external\category_exporter($category, ['context' => $context]); $categorydata = $export->export($output); $section['category'] = $categorydata; $data['contexts'][] = $section; } // Get activity module plugin info. $pluginmanager = \core_plugin_manager::instance(); $modplugins = $pluginmanager->get_enabled_plugins('mod'); foreach ($modplugins as $name) { $classname = \context_helper::get_class_for_level($contextlevels['contextlevelname70']); list($purposevar, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname, $name); $categoryid = get_config('tool_dataprivacy', $categoryvar); $purposeid = get_config('tool_dataprivacy', $purposevar); if ($categoryid === false && $purposeid === false) { // If no purpose and category has been set for this plugin, then there's no need to show this on the list. continue; } $section = []; $section['contextname'] = $pluginmanager->plugin_name('mod_' . $name); if ($purposeid == -1) { $purposeid = 0; } $purpose = new \tool_dataprivacy\purpose($purposeid); $export = new \tool_dataprivacy\external\purpose_exporter($purpose, ['context' => $context]); $purposedata = $export->export($output); $section['purpose'] = $purposedata; if ($categoryid == -1) { $categoryid = 0; } $category = new \tool_dataprivacy\category($categoryid); $export = new \tool_dataprivacy\external\category_exporter($category, ['context' => $context]); $categorydata = $export->export($output); $section['category'] = $categorydata; $data['contexts'][] = $section; } return $data; } } classes/output/data_registry_page.php 0000644 00000037010 15152701722 0014112 0 ustar 00 <?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/>. /** * Data registry renderable. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\output; defined('MOODLE_INTERNAL') || die(); use renderable; use renderer_base; use stdClass; use templatable; use tool_dataprivacy\data_registry; require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php'); require_once($CFG->libdir . '/blocklib.php'); /** * Class containing the data registry renderable * * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class data_registry_page implements renderable, templatable { /** * @var int */ private $defaultcontextlevel; /** * @var int */ private $defaultcontextid; /** * Constructor. * * @param int $defaultcontextlevel * @param int $defaultcontextid * @return null */ public function __construct($defaultcontextlevel = false, $defaultcontextid = false) { $this->defaultcontextlevel = $defaultcontextlevel; $this->defaultcontextid = $defaultcontextid; } /** * Export this data so it can be used as the context for a mustache template. * * @param renderer_base $output * @return stdClass */ public function export_for_template(renderer_base $output) { global $PAGE; $params = [\context_system::instance()->id, $this->defaultcontextlevel, $this->defaultcontextid]; $PAGE->requires->js_call_amd('tool_dataprivacy/data_registry', 'init', $params); $data = new stdClass(); $defaultsbutton = new \action_link( new \moodle_url('/admin/tool/dataprivacy/defaults.php'), get_string('setdefaults', 'tool_dataprivacy'), null, ['class' => 'btn btn-primary'] ); $data->defaultsbutton = $defaultsbutton->export_for_template($output); $actionmenu = new \action_menu(); $actionmenu->set_menu_trigger(get_string('edit'), 'btn btn-primary'); $actionmenu->set_owner_selector('dataregistry-actions'); $url = new \moodle_url('/admin/tool/dataprivacy/categories.php'); $categories = new \action_menu_link_secondary($url, null, get_string('categories', 'tool_dataprivacy')); $actionmenu->add($categories); $url = new \moodle_url('/admin/tool/dataprivacy/purposes.php'); $purposes = new \action_menu_link_secondary($url, null, get_string('purposes', 'tool_dataprivacy')); $actionmenu->add($purposes); $data->actions = $actionmenu->export_for_template($output); if (!data_registry::defaults_set()) { $data->info = (object)[ 'message' => get_string('dataregistryinfo', 'tool_dataprivacy'), 'announce' => 1 ]; $data->nosystemdefaults = (object)[ 'message' => get_string('nosystemdefaults', 'tool_dataprivacy'), 'announce' => 1 ]; } $data->tree = $this->get_default_tree_structure(); return $data; } /** * Returns the tree default structure. * * @return array */ private function get_default_tree_structure() { $frontpage = \context_course::instance(SITEID); $categorybranches = $this->get_all_category_branches(); $elements = [ 'text' => get_string('contextlevelname' . CONTEXT_SYSTEM, 'tool_dataprivacy'), 'contextlevel' => CONTEXT_SYSTEM, 'branches' => [ [ 'text' => get_string('user'), 'contextlevel' => CONTEXT_USER, ], [ 'text' => get_string('categories'), 'branches' => $categorybranches, 'expandelement' => 'category', ], [ 'text' => get_string('frontpagecourse', 'tool_dataprivacy'), 'contextid' => $frontpage->id, 'branches' => [ [ 'text' => get_string('activitiesandresources', 'tool_dataprivacy'), 'expandcontextid' => $frontpage->id, 'expandelement' => 'module', 'expanded' => 0, ], [ 'text' => get_string('blocks'), 'expandcontextid' => $frontpage->id, 'expandelement' => 'block', 'expanded' => 0, ], ] ] ] ]; // Returned as an array to follow a common array format. return [self::complete($elements, $this->defaultcontextlevel, $this->defaultcontextid)]; } /** * Returns the hierarchy of system course categories. * * @return array */ private function get_all_category_branches() { $categories = data_registry::get_site_categories(); $categoriesbranch = []; while (count($categories) > 0) { foreach ($categories as $key => $category) { $context = \context_coursecat::instance($category->id); $newnode = [ 'text' => shorten_text(format_string($category->name, true, ['context' => $context])), 'categoryid' => $category->id, 'contextid' => $context->id, ]; if ($category->coursecount > 0) { $newnode['branches'] = [ [ 'text' => get_string('courses'), 'expandcontextid' => $context->id, 'expandelement' => 'course', 'expanded' => 0, ] ]; } $added = false; if ($category->parent == 0) { // New categories root-level node. $categoriesbranch[] = $newnode; $added = true; } else { // Add the new node under the appropriate parent. if ($this->add_to_parent_category_branch($category, $newnode, $categoriesbranch)) { $added = true; } } if ($added) { unset($categories[$key]); } } } return $categoriesbranch; } /** * Gets the courses branch for the provided category. * * @param \context $catcontext * @return array */ public static function get_courses_branch(\context $catcontext) { if ($catcontext->contextlevel !== CONTEXT_COURSECAT) { throw new \coding_exception('A course category context should be provided'); } $coursecat = \core_course_category::get($catcontext->instanceid); $courses = $coursecat->get_courses(); $branches = []; foreach ($courses as $course) { $coursecontext = \context_course::instance($course->id); $coursenode = [ 'text' => shorten_text(format_string($course->shortname, true, ['context' => $coursecontext])), 'contextid' => $coursecontext->id, 'branches' => [ [ 'text' => get_string('activitiesandresources', 'tool_dataprivacy'), 'expandcontextid' => $coursecontext->id, 'expandelement' => 'module', 'expanded' => 0, ], [ 'text' => get_string('blocks'), 'expandcontextid' => $coursecontext->id, 'expandelement' => 'block', 'expanded' => 0, ], ] ]; $branches[] = self::complete($coursenode); } return $branches; } /** * Gets the modules branch for the provided course. * * @param \context $coursecontext * @return array */ public static function get_modules_branch(\context $coursecontext) { if ($coursecontext->contextlevel !== CONTEXT_COURSE) { throw new \coding_exception('A course context should be provided'); } $branches = []; // Using the current user. $modinfo = get_fast_modinfo($coursecontext->instanceid); foreach ($modinfo->get_instances() as $moduletype => $instances) { foreach ($instances as $cm) { if (!$cm->uservisible) { continue; } $a = (object)[ 'instancename' => shorten_text($cm->get_formatted_name()), 'modulename' => get_string('pluginname', 'mod_' . $moduletype), ]; $text = get_string('moduleinstancename', 'tool_dataprivacy', $a); $branches[] = self::complete([ 'text' => $text, 'contextid' => $cm->context->id, ]); } } return $branches; } /** * Gets the blocks branch for the provided course. * * @param \context $coursecontext * @return null */ public static function get_blocks_branch(\context $coursecontext) { global $DB; if ($coursecontext->contextlevel !== CONTEXT_COURSE) { throw new \coding_exception('A course context should be provided'); } $branches = []; $children = $coursecontext->get_child_contexts(); foreach ($children as $childcontext) { if ($childcontext->contextlevel !== CONTEXT_BLOCK) { continue; } $blockinstance = block_instance_by_id($childcontext->instanceid); $displayname = shorten_text(format_string($blockinstance->get_title(), true, ['context' => $childcontext])); $branches[] = self::complete([ 'text' => $displayname, 'contextid' => $childcontext->id, ]); } return $branches; } /** * Adds the provided category to the categories branch. * * @param stdClass $category * @param array $newnode * @param array $categoriesbranch * @return bool */ private function add_to_parent_category_branch($category, $newnode, &$categoriesbranch) { foreach ($categoriesbranch as $key => $branch) { if (!empty($branch['categoryid']) && $branch['categoryid'] == $category->parent) { // It may be empty (if it does not contain courses and this is the first child cat). if (!isset($categoriesbranch[$key]['branches'])) { $categoriesbranch[$key]['branches'] = []; } $categoriesbranch[$key]['branches'][] = $newnode; return true; } if (!empty($branch['branches'])) { $parent = $this->add_to_parent_category_branch($category, $newnode, $categoriesbranch[$key]['branches']); if ($parent) { return true; } } } return false; } /** * Completes tree nodes with default values. * * @param array $node * @param int|false $currentcontextlevel * @param int|false $currentcontextid * @return array */ private static function complete($node, $currentcontextlevel = false, $currentcontextid = false) { if (!isset($node['active'])) { if ($currentcontextlevel && !empty($node['contextlevel']) && $currentcontextlevel == $node['contextlevel'] && empty($currentcontextid)) { // This is the active context level, we also checked that there // is no default contextid set. $node['active'] = true; } else if ($currentcontextid && !empty($node['contextid']) && $currentcontextid == $node['contextid']) { $node['active'] = true; } else { $node['active'] = null; } } if (!isset($node['branches'])) { $node['branches'] = []; } else { foreach ($node['branches'] as $key => $childnode) { $node['branches'][$key] = self::complete($childnode, $currentcontextlevel, $currentcontextid); } } if (!isset($node['expandelement'])) { $node['expandelement'] = null; } if (!isset($node['expandcontextid'])) { $node['expandcontextid'] = null; } if (!isset($node['contextid'])) { $node['contextid'] = null; } if (!isset($node['contextlevel'])) { $node['contextlevel'] = null; } if (!isset($node['expanded'])) { if (!empty($node['branches'])) { $node['expanded'] = 1; } else { $node['expanded'] = 0; } } return $node; } /** * From a list of purpose persistents to a list of id => name purposes. * * @param \tool_dataprivacy\purpose[] $purposes * @param bool $includenotset * @param bool $includeinherit * @return string[] */ public static function purpose_options($purposes, $includenotset = true, $includeinherit = true) { $options = self::base_options($includenotset, $includeinherit); foreach ($purposes as $purpose) { $options[$purpose->get('id')] = $purpose->get('name'); } return $options; } /** * From a list of category persistents to a list of id => name categories. * * @param \tool_dataprivacy\category[] $categories * @param bool $includenotset * @param bool $includeinherit * @return string[] */ public static function category_options($categories, $includenotset = true, $includeinherit = true) { $options = self::base_options($includenotset, $includeinherit); foreach ($categories as $category) { $options[$category->get('id')] = $category->get('name'); } return $options; } /** * Base not set and inherit options. * * @param bool $includenotset * @param bool $includeinherit * @return array */ private static function base_options($includenotset = true, $includeinherit = true) { $options = []; if ($includenotset) { $options[\tool_dataprivacy\context_instance::NOTSET] = get_string('notset', 'tool_dataprivacy'); } if ($includeinherit) { $options[\tool_dataprivacy\context_instance::INHERIT] = get_string('inherit', 'tool_dataprivacy'); } return $options; } } classes/output/expired_contexts_table.php 0000644 00000034226 15152701722 0015021 0 ustar 00 <?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/>. /** * Contains the class used for the displaying the expired contexts table. * * @package tool_dataprivacy * @copyright 2018 Jun Pataleta <jun@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\output; defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir . '/tablelib.php'); use coding_exception; use context_helper; use dml_exception; use Exception; use html_writer; use pix_icon; use stdClass; use table_sql; use tool_dataprivacy\api; use tool_dataprivacy\expired_context; use tool_dataprivacy\external\purpose_exporter; use tool_dataprivacy\purpose; defined('MOODLE_INTERNAL') || die; /** * The class for displaying the expired contexts table. * * @copyright 2018 Jun Pataleta <jun@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class expired_contexts_table extends table_sql { /** @var int The context level acting as a filter for this table. */ protected $contextlevel = null; /** * @var bool $selectall Has the user selected all users on the page? True by default. */ protected $selectall = true; /** @var purpose[] Array of purposes by their id. */ protected $purposes = []; /** @var purpose[] Map of context => purpose. */ protected $purposemap = []; /** @var array List of roles. */ protected $roles = []; /** * expired_contexts_table constructor. * * @param int|null $contextlevel * @throws coding_exception */ public function __construct($contextlevel = null) { parent::__construct('expired-contexts-table'); $this->contextlevel = $contextlevel; $columnheaders = [ 'name' => get_string('name'), 'info' => get_string('info'), 'purpose' => get_string('purpose', 'tool_dataprivacy'), 'category' => get_string('category', 'tool_dataprivacy'), 'retentionperiod' => get_string('retentionperiod', 'tool_dataprivacy'), 'tobedeleted' => get_string('tobedeleted', 'tool_dataprivacy'), 'timecreated' => get_string('expiry', 'tool_dataprivacy'), ]; $checkboxattrs = [ 'title' => get_string('selectall'), 'data-action' => 'selectall' ]; $columnheaders['select'] = html_writer::checkbox('selectall', 1, true, null, $checkboxattrs); $this->define_columns(array_keys($columnheaders)); $this->define_headers(array_values($columnheaders)); $this->no_sorting('name'); $this->no_sorting('select'); $this->no_sorting('info'); $this->no_sorting('purpose'); $this->no_sorting('category'); $this->no_sorting('retentionperiod'); $this->no_sorting('tobedeleted'); // Make this table sorted by first name by default. $this->sortable(true, 'timecreated'); // We use roles in several places. $this->roles = role_get_names(); } /** * The context name column. * * @param stdClass $expiredctx The row data. * @return string * @throws coding_exception */ public function col_name($expiredctx) { global $OUTPUT; $context = context_helper::instance_by_id($expiredctx->get('contextid')); $parent = $context->get_parent_context(); $contextdata = (object)[ 'name' => $context->get_context_name(false, true), 'parent' => $parent->get_context_name(false, true), ]; $fullcontexts = $context->get_parent_contexts(true); $contextsinpath = []; foreach ($fullcontexts as $contextinpath) { $contextsinpath[] = $contextinpath->get_context_name(false, true); } $infoicon = new pix_icon('i/info', implode(' / ', array_reverse($contextsinpath))); $infoiconhtml = $OUTPUT->render($infoicon); $name = html_writer::span(get_string('nameandparent', 'tool_dataprivacy', $contextdata), 'mr-1'); return $name . $infoiconhtml; } /** * The context information column. * * @param stdClass $expiredctx The row data. * @return string * @throws coding_exception */ public function col_info($expiredctx) { global $OUTPUT; $context = context_helper::instance_by_id($expiredctx->get('contextid')); $children = $context->get_child_contexts(); if (empty($children)) { return get_string('none'); } else { $childnames = []; foreach ($children as $child) { $childnames[] = $child->get_context_name(false, true); } $infoicon = new pix_icon('i/info', implode(', ', $childnames)); $infoiconhtml = $OUTPUT->render($infoicon); $name = html_writer::span(get_string('nchildren', 'tool_dataprivacy', count($children)), 'mr-1'); return $name . $infoiconhtml; } } /** * The category name column. * * @param stdClass $expiredctx The row data. * @return mixed * @throws coding_exception * @throws dml_exception */ public function col_category($expiredctx) { $context = context_helper::instance_by_id($expiredctx->get('contextid')); $category = api::get_effective_context_category($context); return s($category->get('name')); } /** * The purpose column. * * @param stdClass $expiredctx The row data. * @return string * @throws coding_exception */ public function col_purpose($expiredctx) { $purpose = $this->get_purpose_for_expiry($expiredctx); return s($purpose->get('name')); } /** * The retention period column. * * @param stdClass $expiredctx The row data. * @return string */ public function col_retentionperiod($expiredctx) { $purpose = $this->get_purpose_for_expiry($expiredctx); $expiries = []; $expiry = html_writer::tag('dt', get_string('default'), ['class' => 'col-sm-3']); if ($expiredctx->get('defaultexpired')) { $expiries[get_string('default')] = get_string('expiredrolewithretention', 'tool_dataprivacy', (object) [ 'retention' => api::format_retention_period(new \DateInterval($purpose->get('retentionperiod'))), ]); } else { $expiries[get_string('default')] = get_string('unexpiredrolewithretention', 'tool_dataprivacy', (object) [ 'retention' => api::format_retention_period(new \DateInterval($purpose->get('retentionperiod'))), ]); } if (!$expiredctx->is_fully_expired()) { $purposeoverrides = $purpose->get_purpose_overrides(); foreach ($expiredctx->get('unexpiredroles') as $roleid) { $role = $this->roles[$roleid]; $override = $purposeoverrides[$roleid]; $expiries[$role->localname] = get_string('unexpiredrolewithretention', 'tool_dataprivacy', (object) [ 'retention' => api::format_retention_period(new \DateInterval($override->get('retentionperiod'))), ]); } foreach ($expiredctx->get('expiredroles') as $roleid) { $role = $this->roles[$roleid]; $override = $purposeoverrides[$roleid]; $expiries[$role->localname] = get_string('expiredrolewithretention', 'tool_dataprivacy', (object) [ 'retention' => api::format_retention_period(new \DateInterval($override->get('retentionperiod'))), ]); } } $output = array_map(function($rolename, $expiry) { $return = html_writer::tag('dt', $rolename, ['class' => 'col-sm-3']); $return .= html_writer::tag('dd', $expiry, ['class' => 'col-sm-9']); return $return; }, array_keys($expiries), $expiries); return html_writer::tag('dl', implode($output), ['class' => 'row']); } /** * The timecreated a.k.a. the context expiry date column. * * @param stdClass $expiredctx The row data. * @return string */ public function col_timecreated($expiredctx) { return userdate($expiredctx->get('timecreated')); } /** * Generate the select column. * * @param stdClass $expiredctx The row data. * @return string */ public function col_select($expiredctx) { $id = $expiredctx->get('id'); return html_writer::checkbox('expiredcontext_' . $id, $id, $this->selectall, '', ['class' => 'selectcontext']); } /** * Formatting for the 'tobedeleted' column which indicates in a friendlier fashion whose data will be removed. * * @param stdClass $expiredctx The row data. * @return string */ public function col_tobedeleted($expiredctx) { if ($expiredctx->is_fully_expired()) { return get_string('defaultexpired', 'tool_dataprivacy'); } $purpose = $this->get_purpose_for_expiry($expiredctx); $a = (object) []; $expiredroles = []; foreach ($expiredctx->get('expiredroles') as $roleid) { $expiredroles[] = html_writer::tag('li', $this->roles[$roleid]->localname); } $a->expired = html_writer::tag('ul', implode($expiredroles)); $unexpiredroles = []; foreach ($expiredctx->get('unexpiredroles') as $roleid) { $unexpiredroles[] = html_writer::tag('li', $this->roles[$roleid]->localname); } $a->unexpired = html_writer::tag('ul', implode($unexpiredroles)); if ($expiredctx->get('defaultexpired')) { return get_string('defaultexpiredexcept', 'tool_dataprivacy', $a); } else if (empty($unexpiredroles)) { return get_string('defaultunexpired', 'tool_dataprivacy', $a); } else { return get_string('defaultunexpiredwithexceptions', 'tool_dataprivacy', $a); } } /** * Query the database for results to display in the table. * * @param int $pagesize size of page for paginated displayed table. * @param bool $useinitialsbar do you want to use the initials bar. * @throws dml_exception * @throws coding_exception */ public function query_db($pagesize, $useinitialsbar = true) { // Only count expired contexts that are awaiting confirmation. $total = expired_context::get_record_count_by_contextlevel($this->contextlevel, expired_context::STATUS_EXPIRED); $this->pagesize($pagesize, $total); $sort = $this->get_sql_sort(); if (empty($sort)) { $sort = 'timecreated'; } // Only load expired contexts that are awaiting confirmation. $expiredcontexts = expired_context::get_records_by_contextlevel($this->contextlevel, expired_context::STATUS_EXPIRED, $sort, $this->get_page_start(), $this->get_page_size()); $this->rawdata = []; $contextids = []; foreach ($expiredcontexts as $persistent) { $this->rawdata[] = $persistent; $contextids[] = $persistent->get('contextid'); } $this->preload_contexts($contextids); // Set initial bars. if ($useinitialsbar) { $this->initialbars($total > $pagesize); } } /** * Override default implementation to display a more meaningful information to the user. */ public function print_nothing_to_display() { global $OUTPUT; echo $this->render_reset_button(); $this->print_initials_bar(); echo $OUTPUT->notification(get_string('noexpiredcontexts', 'tool_dataprivacy'), 'warning'); } /** * Override the table's show_hide_link method to prevent the show/hide link for the select column from rendering. * * @param string $column the column name, index into various names. * @param int $index numerical index of the column. * @return string HTML fragment. */ protected function show_hide_link($column, $index) { if ($index < 6) { return parent::show_hide_link($column, $index); } return ''; } /** * Get the purpose for the specified expired context. * * @param expired_context $expiredcontext * @return purpose */ protected function get_purpose_for_expiry(expired_context $expiredcontext) : purpose { $context = context_helper::instance_by_id($expiredcontext->get('contextid')); if (empty($this->purposemap[$context->id])) { $purpose = api::get_effective_context_purpose($context); $this->purposemap[$context->id] = $purpose->get('id'); if (empty($this->purposes[$purpose->get('id')])) { $this->purposes[$purpose->get('id')] = $purpose; } } return $this->purposes[$this->purposemap[$context->id]]; } /** * Preload context records given a set of contextids. * * @param array $contextids */ protected function preload_contexts(array $contextids) { global $DB; if (empty($contextids)) { return; } $ctxfields = \context_helper::get_preload_record_columns_sql('ctx'); list($insql, $inparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED); $sql = "SELECT {$ctxfields} FROM {context} ctx WHERE ctx.id {$insql}"; $contextlist = $DB->get_recordset_sql($sql, $inparams); foreach ($contextlist as $contextdata) { \context_helper::preload_from_record($contextdata); } $contextlist->close(); } } classes/output/data_registry_compliance_page.php 0000644 00000003416 15152701722 0016307 0 ustar 00 <?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/>. /** * Contains the data registry compliance renderable. * * @package tool_dataprivacy * @copyright 2018 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\output; defined('MOODLE_INTERNAL') || die(); use renderable; use renderer_base; use templatable; /** * Class containing the data registry compliance renderable * * @copyright 2018 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class data_registry_compliance_page implements renderable, templatable { /** @var array meta-data to be displayed about the system. */ protected $metadata; /** * Constructor. * * @param array $metadata */ public function __construct($metadata) { $this->metadata = $metadata; } /** * Export this data so it can be used as the context for a mustache template. * * @param renderer_base $output * @return stdClass */ public function export_for_template(renderer_base $output) { return ['types' => $this->metadata]; } } classes/data_request.php 0000644 00000023647 15152701722 0011411 0 ustar 00 <?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/>. /** * Class for loading/storing data requests from the DB. * * @package tool_dataprivacy * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy; defined('MOODLE_INTERNAL') || die(); use lang_string; use core\persistent; /** * Class for loading/storing data requests from the DB. * * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class data_request extends persistent { /** The table name this persistent object maps to. */ const TABLE = 'tool_dataprivacy_request'; /** Data request created manually. */ const DATAREQUEST_CREATION_MANUAL = 0; /** Data request created automatically. */ const DATAREQUEST_CREATION_AUTO = 1; /** * Return the definition of the properties of this model. * * @return array */ protected static function define_properties() { return [ 'type' => [ 'choices' => [ api::DATAREQUEST_TYPE_EXPORT, api::DATAREQUEST_TYPE_DELETE, api::DATAREQUEST_TYPE_OTHERS, ], 'type' => PARAM_INT ], 'comments' => [ 'type' => PARAM_TEXT, 'message' => new lang_string('errorinvalidrequestcomments', 'tool_dataprivacy'), 'default' => '' ], 'commentsformat' => [ 'choices' => [ FORMAT_HTML, FORMAT_MOODLE, FORMAT_PLAIN, FORMAT_MARKDOWN ], 'type' => PARAM_INT, 'default' => FORMAT_PLAIN ], 'userid' => [ 'default' => function() { global $USER; return $USER->id; }, 'type' => PARAM_INT ], 'requestedby' => [ 'default' => 0, 'type' => PARAM_INT ], 'status' => [ 'default' => api::DATAREQUEST_STATUS_AWAITING_APPROVAL, 'choices' => [ api::DATAREQUEST_STATUS_PENDING, api::DATAREQUEST_STATUS_AWAITING_APPROVAL, api::DATAREQUEST_STATUS_APPROVED, api::DATAREQUEST_STATUS_PROCESSING, api::DATAREQUEST_STATUS_COMPLETE, api::DATAREQUEST_STATUS_CANCELLED, api::DATAREQUEST_STATUS_REJECTED, api::DATAREQUEST_STATUS_DOWNLOAD_READY, api::DATAREQUEST_STATUS_EXPIRED, api::DATAREQUEST_STATUS_DELETED, ], 'type' => PARAM_INT ], 'dpo' => [ 'default' => 0, 'type' => PARAM_INT, 'null' => NULL_ALLOWED ], 'dpocomment' => [ 'default' => '', 'type' => PARAM_TEXT, 'null' => NULL_ALLOWED ], 'dpocommentformat' => [ 'choices' => [ FORMAT_HTML, FORMAT_MOODLE, FORMAT_PLAIN, FORMAT_MARKDOWN ], 'type' => PARAM_INT, 'default' => FORMAT_PLAIN ], 'systemapproved' => [ 'default' => false, 'type' => PARAM_BOOL, ], 'creationmethod' => [ 'default' => self::DATAREQUEST_CREATION_MANUAL, 'choices' => [ self::DATAREQUEST_CREATION_MANUAL, self::DATAREQUEST_CREATION_AUTO ], 'type' => PARAM_INT ], ]; } /** * Determines whether a completed data export request has expired. * The response will be valid regardless of the expiry scheduled task having run. * * @param data_request $request the data request object whose expiry will be checked. * @return bool true if the request has expired. */ public static function is_expired(data_request $request) { $result = false; // Only export requests expire. if ($request->get('type') == api::DATAREQUEST_TYPE_EXPORT) { switch ($request->get('status')) { // Expired requests are obviously expired. case api::DATAREQUEST_STATUS_EXPIRED: $result = true; break; // Complete requests are expired if the expiry time has elapsed. case api::DATAREQUEST_STATUS_DOWNLOAD_READY: $expiryseconds = get_config('tool_dataprivacy', 'privacyrequestexpiry'); if ($expiryseconds > 0 && time() >= ($request->get('timemodified') + $expiryseconds)) { $result = true; } break; } } return $result; } /** * Fetch completed data requests which are due to expire. * * @param int $userid Optional user ID to filter by. * * @return array Details of completed requests which are due to expire. */ public static function get_expired_requests($userid = 0) { global $DB; $expiryseconds = get_config('tool_dataprivacy', 'privacyrequestexpiry'); $expirytime = strtotime("-{$expiryseconds} second"); $table = self::TABLE; $sqlwhere = 'type = :export_type AND status = :completestatus AND timemodified <= :expirytime'; $params = array( 'export_type' => api::DATAREQUEST_TYPE_EXPORT, 'completestatus' => api::DATAREQUEST_STATUS_DOWNLOAD_READY, 'expirytime' => $expirytime, ); $sort = 'id'; $fields = 'id, userid'; // Filter by user ID if specified. if ($userid > 0) { $sqlwhere .= ' AND (userid = :userid OR requestedby = :requestedby)'; $params['userid'] = $userid; $params['requestedby'] = $userid; } return $DB->get_records_select_menu($table, $sqlwhere, $params, $sort, $fields, 0, 2000); } /** * Expire a given set of data requests. * Update request status and delete the files. * * @param array $expiredrequests [requestid => userid] * * @return void */ public static function expire($expiredrequests) { global $DB; $ids = array_keys($expiredrequests); if (count($ids) > 0) { list($insql, $inparams) = $DB->get_in_or_equal($ids); $initialparams = array(api::DATAREQUEST_STATUS_EXPIRED, time()); $params = array_merge($initialparams, $inparams); $update = "UPDATE {" . self::TABLE . "} SET status = ?, timemodified = ? WHERE id $insql"; if ($DB->execute($update, $params)) { $fs = get_file_storage(); foreach ($expiredrequests as $id => $userid) { $usercontext = \context_user::instance($userid); $fs->delete_area_files($usercontext->id, 'tool_dataprivacy', 'export', $id); } } } } /** * Whether this request is in a state appropriate for reset/resubmission. * * Note: This does not check whether any other completed requests exist for this user. * * @return bool */ public function is_resettable() : bool { if (api::DATAREQUEST_TYPE_OTHERS == $this->get('type')) { // It is not possible to reset 'other' reqeusts. return false; } $resettable = [ api::DATAREQUEST_STATUS_APPROVED => true, api::DATAREQUEST_STATUS_REJECTED => true, ]; return isset($resettable[$this->get('status')]); } /** * Whether this request is 'active'. * * @return bool */ public function is_active() : bool { $active = [ api::DATAREQUEST_STATUS_APPROVED => true, ]; return isset($active[$this->get('status')]); } /** * Reject this request and resubmit it as a fresh request. * * Note: This does not check whether any other completed requests exist for this user. * * @return self */ public function resubmit_request() : data_request { if ($this->is_active()) { $this->set('status', api::DATAREQUEST_STATUS_REJECTED)->save(); } if (!$this->is_resettable()) { throw new \moodle_exception('cannotreset', 'tool_dataprivacy'); } $currentdata = $this->to_record(); unset($currentdata->id); // Clone the original request, but do not notify. $clone = api::create_data_request( $this->get('userid'), $this->get('type'), $this->get('comments'), $this->get('creationmethod'), false ); $clone->set('comments', $this->get('comments')); $clone->set('dpo', $this->get('dpo')); $clone->set('requestedby', $this->get('requestedby')); $clone->save(); return $clone; } } classes/privacy/provider.php 0000644 00000025214 15152701722 0012227 0 ustar 00 <?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 class for requesting user data. * * @package tool_dataprivacy * @copyright 2018 Jake Dallimore <jrhdallimore@gmail.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\privacy; defined('MOODLE_INTERNAL') || die(); use coding_exception; use context; use context_user; use core_privacy\local\metadata\collection; use core_privacy\local\request\approved_contextlist; use \core_privacy\local\request\approved_userlist; use core_privacy\local\request\contextlist; use core_privacy\local\request\helper; use core_privacy\local\request\transform; use \core_privacy\local\request\userlist; use core_privacy\local\request\writer; use dml_exception; use stdClass; use tool_dataprivacy\api; use tool_dataprivacy\local\helper as tool_helper; /** * Privacy class for requesting user data. * * @package tool_dataprivacy * @copyright 2018 Jake Dallimore <jrhdallimore@gmail.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements // This tool stores user data. \core_privacy\local\metadata\provider, // This plugin is capable of determining which users have data within it. \core_privacy\local\request\core_userlist_provider, // This tool may provide access to and deletion of user data. \core_privacy\local\request\plugin\provider, // This plugin has some sitewide user preferences to export. \core_privacy\local\request\user_preference_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 { $collection->add_database_table( 'tool_dataprivacy_request', [ 'comments' => 'privacy:metadata:request:comments', 'userid' => 'privacy:metadata:request:userid', 'requestedby' => 'privacy:metadata:request:requestedby', 'dpocomment' => 'privacy:metadata:request:dpocomment', 'timecreated' => 'privacy:metadata:request:timecreated' ], 'privacy:metadata:request' ); // Regarding this block, we are unable to export or purge this data, as // it would damage the privacy data across the whole site. $collection->add_database_table( 'tool_dataprivacy_purposerole', [ 'usermodified' => 'privacy:metadata:purpose:usermodified', ], 'privacy:metadata:purpose' ); $collection->add_user_preference(tool_helper::PREF_REQUEST_FILTERS, 'privacy:metadata:preference:tool_dataprivacy_request-filters'); $collection->add_user_preference(tool_helper::PREF_REQUEST_PERPAGE, 'privacy:metadata:preference:tool_dataprivacy_request-perpage'); return $collection; } /** * 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 { $sql = "SELECT id FROM {context} WHERE instanceid = :userid AND contextlevel = :contextlevel"; $contextlist = new contextlist(); $contextlist->set_component('tool_dataprivacy'); $contextlist->add_from_sql($sql, ['userid' => $userid, 'contextlevel' => CONTEXT_USER]); return $contextlist; } /** * Get the list of users who have data within a context. * * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. * */ public static function get_users_in_context(userlist $userlist) { $context = $userlist->get_context(); if (!is_a($context, \context_user::class)) { return; } $params = [ 'contextlevel' => CONTEXT_USER, 'contextid' => $context->id, ]; $sql = "SELECT instanceid AS userid FROM {context} WHERE id = :contextid AND contextlevel = :contextlevel"; $userlist->add_from_sql('userid', $sql, $params); } /** * Export all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts to export information for. * @throws coding_exception * @throws dml_exception * @throws \moodle_exception */ public static function export_user_data(approved_contextlist $contextlist) { if (empty($contextlist->count())) { return; } $user = $contextlist->get_user(); $datarequests = api::get_data_requests($user->id); $context = context_user::instance($user->id); $contextdatatowrite = []; foreach ($datarequests as $request) { $record = $request->to_record(); $data = new stdClass(); // The user ID that made the request/the request is made for. if ($record->requestedby != $record->userid) { if ($user->id != $record->requestedby) { // This request is done by this user for another user. $data->userid = fullname($user); } else if ($user->id != $record->userid) { // This request was done by another user on behalf of this user. $data->requestedby = fullname($user); } } // Request type. $data->type = tool_helper::get_shortened_request_type_string($record->type); // Status. $data->status = tool_helper::get_request_status_string($record->status); // Creation method. $data->creationmethod = tool_helper::get_request_creation_method_string($record->creationmethod); // Comments. $data->comments = $record->comments; // The DPO's comment about this request. $data->dpocomment = $record->dpocomment; // The date and time this request was lodged. $data->timecreated = transform::datetime($record->timecreated); $contextdatatowrite[] = $data; } // User context / Privacy and policies / Data requests. $subcontext = [ get_string('privacyandpolicies', 'admin'), get_string('datarequests', 'tool_dataprivacy'), ]; writer::with_context($context)->export_data($subcontext, (object)$contextdatatowrite); // Write generic module intro files. helper::export_context_files($context, $user); } /** * Delete all data for all users in the specified context. * * @param context $context The specific context to delete data for. */ public static function delete_data_for_all_users_in_context(context $context) { } /** * 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_data_for_user(approved_contextlist $contextlist) { } /** * Delete multiple users within a single context. * * @param approved_userlist $userlist The approved context and user information to delete information for. * */ public static function delete_data_for_users(approved_userlist $userlist) { } /** * 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) { $preffilter = get_user_preferences(tool_helper::PREF_REQUEST_FILTERS, null, $userid); if ($preffilter !== null) { $filters = json_decode($preffilter); $descriptions = []; foreach ($filters as $filter) { list($category, $value) = explode(':', $filter); $option = new stdClass(); switch($category) { case tool_helper::FILTER_TYPE: $option->category = get_string('requesttype', 'tool_dataprivacy'); $option->name = tool_helper::get_shortened_request_type_string($value); break; case tool_helper::FILTER_STATUS: $option->category = get_string('requeststatus', 'tool_dataprivacy'); $option->name = tool_helper::get_request_status_string($value); break; case tool_helper::FILTER_CREATION: $option->category = get_string('requestcreation', 'tool_dataprivacy'); $option->name = tool_helper::get_request_creation_method_string($value); break; } $descriptions[] = get_string('filteroption', 'tool_dataprivacy', $option); } // Export the filter preference as comma-separated values and text descriptions. $values = implode(', ', $filters); $descriptionstext = implode(', ', $descriptions); writer::export_user_preference('tool_dataprivacy', tool_helper::PREF_REQUEST_FILTERS, $values, $descriptionstext); } $prefperpage = get_user_preferences(tool_helper::PREF_REQUEST_PERPAGE, null, $userid); if ($prefperpage !== null) { writer::export_user_preference('tool_dataprivacy', tool_helper::PREF_REQUEST_PERPAGE, $prefperpage, get_string('privacy:metadata:preference:tool_dataprivacy_request-perpage', 'tool_dataprivacy')); } } } classes/context_instance.php 0000644 00000006317 15152701722 0012273 0 ustar 00 <?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/>. /** * Class for loading/storing context instances data from the DB. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy; defined('MOODLE_INTERNAL') || die(); /** * Class for loading/storing context instances data from the DB. * * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class context_instance extends \core\persistent { /** * Database table. */ const TABLE = 'tool_dataprivacy_ctxinstance'; /** * Not set value. */ const NOTSET = 0; /** * Inherit value. */ const INHERIT = -1; /** * Return the definition of the properties of this model. * * @return array */ protected static function define_properties() { return array( 'contextid' => array( 'type' => PARAM_INT, 'description' => 'The context id.', ), 'purposeid' => array( 'type' => PARAM_INT, 'description' => 'The purpose id.', 'null' => NULL_ALLOWED, ), 'categoryid' => array( 'type' => PARAM_INT, 'description' => 'The category id.', 'null' => NULL_ALLOWED, ), ); } /** * Returns an instance by contextid. * * @param mixed $contextid * @param mixed $exception * @return null */ public static function get_record_by_contextid($contextid, $exception = true) { global $DB; if (!$record = $DB->get_record(self::TABLE, array('contextid' => $contextid))) { if (!$exception) { return false; } else { throw new \dml_missing_record_exception(self::TABLE); } } return new static(0, $record); } /** * Is the provided purpose used by any context instance? * * @param int $purposeid * @return bool */ public static function is_purpose_used($purposeid) { global $DB; return $DB->record_exists(self::TABLE, array('purposeid' => $purposeid)); } /** * Is the provided category used by any context instance? * * @param int $categoryid * @return bool */ public static function is_category_used($categoryid) { global $DB; return $DB->record_exists(self::TABLE, array('categoryid' => $categoryid)); } } classes/page_helper.php 0000644 00000006134 15152701722 0011173 0 ustar 00 <?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/>. /** * Page helper. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy; use context_system; use moodle_url; defined('MOODLE_INTERNAL') || die(); /** * Page helper. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class page_helper { /** * Sets up $PAGE for data privacy admin pages. * * @param moodle_url $url The page URL. * @param string $title The page's title. * @param string $attachtoparentnode The parent navigation node where this page can be accessed from. * @param string $requiredcapability The required capability to view this page. */ public static function setup(moodle_url $url, $title, $attachtoparentnode = '', $requiredcapability = 'tool/dataprivacy:managedataregistry') { global $PAGE, $SITE; $context = context_system::instance(); require_login(); if (isguestuser()) { throw new \moodle_exception('noguest'); } // TODO Check that data privacy is enabled. require_capability($requiredcapability, $context); $PAGE->navigation->override_active_url($url); $PAGE->set_url($url); $PAGE->set_context($context); $PAGE->set_pagelayout('admin'); $PAGE->set_title($title); $PAGE->set_heading($SITE->fullname); $PAGE->set_secondary_active_tab('users'); $PAGE->set_primary_active_tab('siteadminnode'); // If necessary, override the settings navigation to add this page into the breadcrumb navigation. if ($attachtoparentnode) { if ($siteadmin = $PAGE->settingsnav->find('root', \navigation_node::TYPE_SITE_ADMIN)) { $PAGE->navbar->add($siteadmin->get_content(), $siteadmin->action()); } if ($dataprivacy = $PAGE->settingsnav->find('privacy', \navigation_node::TYPE_SETTING)) { $PAGE->navbar->add($dataprivacy->get_content(), $dataprivacy->action()); } if ($dataregistry = $PAGE->settingsnav->find($attachtoparentnode, \navigation_node::TYPE_SETTING)) { $PAGE->navbar->add($dataregistry->get_content(), $dataregistry->action()); } $PAGE->navbar->add($title, $url); } } } classes/expiry_info.php 0000644 00000014461 15152701722 0011255 0 ustar 00 <?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/>. /** * Expiry Data. * * @package tool_dataprivacy * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy; use core_privacy\manager; defined('MOODLE_INTERNAL') || die(); /** * Expiry Data. * * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class expiry_info { /** @var bool Whether this context is fully expired */ protected $fullyexpired = false; /** @var bool Whether the default expiry value of this purpose has been reached */ protected $defaultexpiryreached = false; /** @var bool Whether the default purpose is protected */ protected $defaultprotected = false; /** @var int[] List of expires roles */ protected $expired = []; /** @var int[] List of unexpires roles */ protected $unexpired = []; /** @var int[] List of unexpired roles which are also protected */ protected $protectedroles = []; /** * Constructor for the expiry_info class. * * @param bool $default Whether the default expiry period for this context has been reached. * @param bool $defaultprotected Whether the default expiry is protected. * @param int[] $expired A list of roles in this context which have explicitly expired. * @param int[] $unexpired A list of roles in this context which have not yet expired. * @param int[] $protectedroles A list of unexpired roles in this context which are protected. */ public function __construct(bool $default, bool $defaultprotected, array $expired, array $unexpired, array $protectedroles) { $this->defaultexpiryreached = $default; $this->defaultprotected = $defaultprotected; $this->expired = $expired; $this->unexpired = $unexpired; $this->protectedroles = $protectedroles; } /** * Whether this context has 'fully' expired. * That is to say that the default retention period has been reached, and that there are no unexpired roles. * * @return bool */ public function is_fully_expired() : bool { return $this->defaultexpiryreached && empty($this->unexpired); } /** * Whether any part of this context has expired. * * @return bool */ public function is_any_expired() : bool { if ($this->is_fully_expired()) { return true; } if (!empty($this->get_expired_roles())) { return true; } if ($this->is_default_expired()) { return true; } return false; } /** * Get the list of explicitly expired role IDs. * Note: This does not list roles which have been expired via the default retention policy being reached. * * @return int[] */ public function get_expired_roles() : array { if ($this->is_default_expired()) { return []; } return $this->expired; } /** * Check whether the specified role is explicitly expired. * Note: This does not list roles which have been expired via the default retention policy being reached. * * @param int $roleid * @return bool */ public function is_role_expired(int $roleid) : bool { return false !== array_search($roleid, $this->expired); } /** * Whether the default retention policy has been reached. * * @return bool */ public function is_default_expired() : bool { return $this->defaultexpiryreached; } /** * Whether the default purpose is protected. * * @return bool */ public function is_default_protected() : bool { return $this->defaultprotected; } /** * Get the list of unexpired role IDs. * * @return int[] */ public function get_unexpired_roles() : array { return $this->unexpired; } /** * Get the list of unexpired protected roles. * * @return int[] */ public function get_unexpired_protected_roles() : array { return array_keys(array_filter($this->protectedroles)); } /** * Get a list of all overridden roles which are unprotected. * @return int[] */ public function get_unprotected_overridden_roles() : array { $allroles = array_merge($this->expired, $this->unexpired); return array_diff($allroles, $this->protectedroles); } /** * Merge this expiry_info object with another belonging to a child context in order to set the 'safest' heritage. * * It is not possible to delete any part of a context that is not deleted by a parent. * So if a course's retention policy has been reached, then only parts where the children have also expired can be * deleted. * * @param expiry_info $child The child record to merge with. * @return $this */ public function merge_with_child(expiry_info $child) : expiry_info { if ($child->is_fully_expired()) { return $this; } // If the child is not fully expired, then none of the parents can be either. $this->fullyexpired = false; // Remove any role in this node which is not expired in the child. foreach ($this->expired as $key => $roleid) { if (!$child->is_role_expired($roleid)) { unset($this->expired[$key]); } } array_merge($this->unexpired, $child->get_unexpired_roles()); if (!$child->is_default_expired()) { $this->defaultexpiryreached = false; } return $this; } } classes/purpose_override.php 0000644 00000011131 15152701722 0012305 0 ustar 00 <?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/>. /** * Class for loading/storing data purpose overrides from the DB. * * @package tool_dataprivacy * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy; use stdClass; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php'); /** * Class for loading/storing data purpose overrides from the DB. * * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class purpose_override extends \core\persistent { /** * Database table. */ const TABLE = 'tool_dataprivacy_purposerole'; /** * Return the definition of the properties of this model. * * @return array */ protected static function define_properties() { return array( 'purposeid' => array( 'type' => PARAM_INT, 'description' => 'The purpose that that this override relates to', ), 'roleid' => array( 'type' => PARAM_INT, 'description' => 'The role that that this override relates to', ), 'lawfulbases' => array( 'type' => PARAM_TEXT, 'description' => 'Comma-separated IDs matching records in tool_dataprivacy_lawfulbasis.', 'null' => NULL_ALLOWED, 'default' => null, ), 'sensitivedatareasons' => array( 'type' => PARAM_TEXT, 'description' => 'Comma-separated IDs matching records in tool_dataprivacy_sensitive', 'null' => NULL_ALLOWED, 'default' => null, ), 'retentionperiod' => array( 'type' => PARAM_ALPHANUM, 'description' => 'Retention period. ISO_8601 durations format (as in DateInterval format).', 'default' => '', ), 'protected' => array( 'type' => PARAM_INT, 'description' => 'Data retention with higher precedent over user\'s request to be forgotten.', 'default' => '0', ), ); } /** * Get all role overrides for the purpose. * * @param purpose $purpose * @return array */ public static function get_overrides_for_purpose(purpose $purpose) : array { $cache = \cache::make('tool_dataprivacy', 'purpose_overrides'); $overrides = []; $alldata = $cache->get($purpose->get('id')); if (false === $alldata) { $tocache = []; foreach (self::get_records(['purposeid' => $purpose->get('id')]) as $override) { $tocache[] = $override->to_record(); $overrides[$override->get('roleid')] = $override; } $cache->set($purpose->get('id'), $tocache); } else { foreach ($alldata as $data) { $override = new self(0, $data); $overrides[$override->get('roleid')] = $override; } } return $overrides; } /** * Adds the new record to the cache. * * @return null */ protected function after_create() { $cache = \cache::make('tool_dataprivacy', 'purpose_overrides'); $cache->delete($this->get('purposeid')); } /** * Updates the cache record. * * @param bool $result * @return null */ protected function after_update($result) { $cache = \cache::make('tool_dataprivacy', 'purpose_overrides'); $cache->delete($this->get('purposeid')); } /** * Removes unnecessary stuff from db. * * @return null */ protected function before_delete() { $cache = \cache::make('tool_dataprivacy', 'purpose_overrides'); $cache->delete($this->get('purposeid')); } } classes/form/context_instance.php 0000644 00000020547 15152701722 0013237 0 ustar 00 <?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/>. /** * This file contains the form add/update context instance data. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\form; defined('MOODLE_INTERNAL') || die(); use tool_dataprivacy\api; use tool_dataprivacy\data_registry; use tool_dataprivacy\purpose; /** * Context instance data form. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class context_instance extends \core\form\persistent { /** * @var The persistent class. */ protected static $persistentclass = 'tool_dataprivacy\\context_instance'; /** * Define the form - called by parent constructor */ public function definition() { $this->_form->setDisableShortforms(); $this->_form->addElement('header', 'contextname', $this->_customdata['contextname']); $subjectscope = implode(', ', $this->_customdata['subjectscope']); if (empty($subjectscope)) { $subjectscope = get_string('noassignedroles', 'tool_dataprivacy'); } $this->_form->addElement('static', 'subjectscope', get_string('subjectscope', 'tool_dataprivacy'), $subjectscope); $this->_form->addHelpButton('subjectscope', 'subjectscope', 'tool_dataprivacy'); $this->add_purpose_category($this->_customdata['context']->contextlevel); $this->_form->addElement('hidden', 'contextid'); $this->_form->setType('contextid', PARAM_INT); parent::add_action_buttons(false, get_string('savechanges')); } /** * Adds purpose and category selectors. * * @param int $contextlevel Apply this context level defaults. False for no defaults. * @return null */ protected function add_purpose_category($contextlevel = false) { $mform = $this->_form; $addcategorytext = $this->get_add_element_content(get_string('addcategory', 'tool_dataprivacy')); $categoryselect = $mform->createElement('select', 'categoryid', null, $this->_customdata['categories']); $addcategory = $mform->createElement('button', 'addcategory', $addcategorytext, ['data-add-element' => 'category']); $mform->addElement('group', 'categorygroup', get_string('category', 'tool_dataprivacy'), [$categoryselect, $addcategory], null, false); $mform->addHelpButton('categorygroup', 'category', 'tool_dataprivacy'); $mform->setType('categoryid', PARAM_INT); $mform->setDefault('categoryid', 0); $addpurposetext = $this->get_add_element_content(get_string('addpurpose', 'tool_dataprivacy')); $purposeselect = $mform->createElement('select', 'purposeid', null, $this->_customdata['purposes']); $addpurpose = $mform->createElement('button', 'addpurpose', $addpurposetext, ['data-add-element' => 'purpose']); $mform->addElement('group', 'purposegroup', get_string('purpose', 'tool_dataprivacy'), [$purposeselect, $addpurpose], null, false); $mform->addHelpButton('purposegroup', 'purpose', 'tool_dataprivacy'); $mform->setType('purposeid', PARAM_INT); $mform->setDefault('purposeid', 0); if (!empty($this->_customdata['currentretentionperiod'])) { $mform->addElement('static', 'retention_current', get_string('retentionperiod', 'tool_dataprivacy'), $this->_customdata['currentretentionperiod']); $mform->addHelpButton('retention_current', 'retentionperiod', 'tool_dataprivacy'); } } /** * Returns the 'add' label. * * It depends on the theme in use. * * @param string $label * @return \renderable|string */ private function get_add_element_content($label) { global $PAGE, $OUTPUT; $bs4 = false; $theme = $PAGE->theme; if ($theme->name === 'boost') { $bs4 = true; } else { foreach ($theme->parents as $basetheme) { if ($basetheme === 'boost') { $bs4 = true; } } } if (!$bs4) { return $label; } return $OUTPUT->pix_icon('e/insert', $label); } /** * Returns the customdata array for the provided context instance. * * @param \context $context * @return array */ public static function get_context_instance_customdata(\context $context) { $persistent = \tool_dataprivacy\context_instance::get_record_by_contextid($context->id, false); if (!$persistent) { $persistent = new \tool_dataprivacy\context_instance(); $persistent->set('contextid', $context->id); } $purposes = []; foreach (api::get_purposes() as $purpose) { $purposes[$purpose->get('id')] = $purpose; } $purposeoptions = \tool_dataprivacy\output\data_registry_page::purpose_options($purposes); $categoryoptions = \tool_dataprivacy\output\data_registry_page::category_options(api::get_categories()); $customdata = [ 'context' => $context, 'subjectscope' => data_registry::get_subject_scope($context), 'contextname' => $context->get_context_name(), 'persistent' => $persistent, 'purposes' => $purposeoptions, 'categories' => $categoryoptions, ]; $effectivepurpose = api::get_effective_context_purpose($context); if ($effectivepurpose) { $customdata['currentretentionperiod'] = self::get_retention_display_text($effectivepurpose, $context->contextlevel, $context); $customdata['purposeretentionperiods'] = []; foreach (array_keys($purposeoptions) as $optionvalue) { if (isset($purposes[$optionvalue])) { $purpose = $purposes[$optionvalue]; } else { // Get the effective purpose if $optionvalue would be the selected value. $purpose = api::get_effective_context_purpose($context, $optionvalue); } $retentionperiod = self::get_retention_display_text( $purpose, $context->contextlevel, $context ); $customdata['purposeretentionperiods'][$optionvalue] = $retentionperiod; } } return $customdata; } /** * Returns the purpose display text. * * @param purpose $effectivepurpose * @param int $retentioncontextlevel * @param \context $context The context, just for displaying (filters) purposes. * @return string */ protected static function get_retention_display_text(purpose $effectivepurpose, $retentioncontextlevel, \context $context) { global $PAGE; $renderer = $PAGE->get_renderer('tool_dataprivacy'); $exporter = new \tool_dataprivacy\external\purpose_exporter($effectivepurpose, ['context' => $context]); $exportedpurpose = $exporter->export($renderer); switch ($retentioncontextlevel) { case CONTEXT_COURSE: case CONTEXT_MODULE: case CONTEXT_BLOCK: $str = get_string('effectiveretentionperiodcourse', 'tool_dataprivacy', $exportedpurpose->formattedretentionperiod); break; case CONTEXT_USER: $str = get_string('effectiveretentionperioduser', 'tool_dataprivacy', $exportedpurpose->formattedretentionperiod); break; default: $str = $exportedpurpose->formattedretentionperiod; } return $str; } } classes/form/purpose.php 0000644 00000044141 15152701722 0011360 0 ustar 00 <?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/>. /** * This file contains the form add/update a data purpose. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\form; defined('MOODLE_INTERNAL') || die(); use core\form\persistent; /** * Data purpose form. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class purpose extends persistent { /** * @var string The persistent class. */ protected static $persistentclass = 'tool_dataprivacy\\purpose'; /** * @var array The list of current overrides. */ protected $existingoverrides = []; /** * Define the form - called by parent constructor */ public function definition() { $mform = $this->_form; $mform->addElement('text', 'name', get_string('name'), 'maxlength="100"'); $mform->setType('name', PARAM_TEXT); $mform->addRule('name', get_string('required'), 'required', null, 'server'); $mform->addRule('name', get_string('maximumchars', '', 100), 'maxlength', 100, 'server'); $mform->addElement('editor', 'description', get_string('description'), null, ['autosave' => false]); $mform->setType('description', PARAM_CLEANHTML); // Field for selecting lawful bases (from GDPR Article 6.1). $this->add_field($this->get_lawful_base_field()); $mform->addRule('lawfulbases', get_string('required'), 'required', null, 'server'); // Optional field for selecting reasons for collecting sensitive personal data (from GDPR Article 9.2). $this->add_field($this->get_sensitive_base_field()); $this->add_field($this->get_retention_period_fields()); $this->add_field($this->get_protected_field()); $this->add_override_fields(); if (!empty($this->_customdata['showbuttons'])) { if (!$this->get_persistent()->get('id')) { $savetext = get_string('add'); } else { $savetext = get_string('savechanges'); } $this->add_action_buttons(true, $savetext); } } /** * Add a fieldset to the current form. * * @param \stdClass $data */ protected function add_field(\stdClass $data) { foreach ($data->fields as $field) { $this->_form->addElement($field); } if (!empty($data->helps)) { foreach ($data->helps as $fieldname => $helpdata) { $help = array_merge([$fieldname], $helpdata); call_user_func_array([$this->_form, 'addHelpButton'], $help); } } if (!empty($data->types)) { foreach ($data->types as $fieldname => $type) { $this->_form->setType($fieldname, $type); } } if (!empty($data->rules)) { foreach ($data->rules as $fieldname => $ruledata) { $rule = array_merge([$fieldname], $ruledata); call_user_func_array([$this->_form, 'addRule'], $rule); } } if (!empty($data->defaults)) { foreach ($data->defaults as $fieldname => $default) { $this->_form($fieldname, $default); } } } /** * Handle addition of relevant repeated element fields for role overrides. */ protected function add_override_fields() { $purpose = $this->get_persistent(); if (empty($purpose->get('id'))) { // It is not possible to use repeated elements in a modal form yet. return; } $fields = [ $this->get_role_override_id('roleoverride_'), $this->get_role_field('roleoverride_'), $this->get_retention_period_fields('roleoverride_'), $this->get_protected_field('roleoverride_'), $this->get_lawful_base_field('roleoverride_'), $this->get_sensitive_base_field('roleoverride_'), ]; $options = [ 'type' => [], 'helpbutton' => [], ]; // Start by adding the title. $overrideelements = [ $this->_form->createElement('header', 'roleoverride', get_string('roleoverride', 'tool_dataprivacy')), $this->_form->createElement( 'static', 'roleoverrideoverview', '', get_string('roleoverrideoverview', 'tool_dataprivacy') ), ]; foreach ($fields as $fielddata) { foreach ($fielddata->fields as $field) { $overrideelements[] = $field; } if (!empty($fielddata->helps)) { foreach ($fielddata->helps as $name => $help) { if (!isset($options[$name])) { $options[$name] = []; } $options[$name]['helpbutton'] = $help; } } if (!empty($fielddata->types)) { foreach ($fielddata->types as $name => $type) { if (!isset($options[$name])) { $options[$name] = []; } $options[$name]['type'] = $type; } } if (!empty($fielddata->rules)) { foreach ($fielddata->rules as $name => $rule) { if (!isset($options[$name])) { $options[$name] = []; } $options[$name]['rule'] = $rule; } } if (!empty($fielddata->defaults)) { foreach ($fielddata->defaults as $name => $default) { if (!isset($options[$name])) { $options[$name] = []; } $options[$name]['default'] = $default; } } if (!empty($fielddata->advanceds)) { foreach ($fielddata->advanceds as $name => $advanced) { if (!isset($options[$name])) { $options[$name] = []; } $options[$name]['advanced'] = $advanced; } } } $this->existingoverrides = $purpose->get_purpose_overrides(); $existingoverridecount = count($this->existingoverrides); $this->repeat_elements( $overrideelements, $existingoverridecount, $options, 'overrides', 'addoverride', 1, get_string('addroleoverride', 'tool_dataprivacy') ); } /** * Converts fields. * * @param \stdClass $data * @return \stdClass */ public function filter_data_for_persistent($data) { $data = parent::filter_data_for_persistent($data); $classname = static::$persistentclass; $properties = $classname::properties_definition(); $data = (object) array_filter((array) $data, function($value, $key) use ($properties) { return isset($properties[$key]); }, ARRAY_FILTER_USE_BOTH); return $data; } /** * Get the field for the role name. * * @param string $prefix The prefix to apply to the field * @return \stdClass */ protected function get_role_override_id(string $prefix = '') : \stdClass { $fieldname = "{$prefix}id"; $fielddata = (object) [ 'fields' => [], ]; $fielddata->fields[] = $this->_form->createElement('hidden', $fieldname); $fielddata->types[$fieldname] = PARAM_INT; return $fielddata; } /** * Get the field for the role name. * * @param string $prefix The prefix to apply to the field * @return \stdClass */ protected function get_role_field(string $prefix = '') : \stdClass { $fieldname = "{$prefix}roleid"; $fielddata = (object) [ 'fields' => [], 'helps' => [], ]; $roles = [ '' => get_string('none'), ]; foreach (role_get_names() as $roleid => $role) { $roles[$roleid] = $role->localname; } $fielddata->fields[] = $this->_form->createElement('select', $fieldname, get_string('role'), $roles, [ 'multiple' => false, ] ); $fielddata->helps[$fieldname] = ['role', 'tool_dataprivacy']; $fielddata->defaults[$fieldname] = null; return $fielddata; } /** * Get the mform field for lawful bases. * * @param string $prefix The prefix to apply to the field * @return \stdClass */ protected function get_lawful_base_field(string $prefix = '') : \stdClass { $fieldname = "{$prefix}lawfulbases"; $data = (object) [ 'fields' => [], ]; $bases = []; foreach (\tool_dataprivacy\purpose::GDPR_ART_6_1_ITEMS as $article) { $key = 'gdpr_art_6_1_' . $article; $bases[$key] = get_string("{$key}_name", 'tool_dataprivacy'); } $data->fields[] = $this->_form->createElement('autocomplete', $fieldname, get_string('lawfulbases', 'tool_dataprivacy'), $bases, [ 'multiple' => true, ] ); $data->helps = [ $fieldname => ['lawfulbases', 'tool_dataprivacy'], ]; $data->advanceds = [ $fieldname => true, ]; return $data; } /** * Get the mform field for sensitive bases. * * @param string $prefix The prefix to apply to the field * @return \stdClass */ protected function get_sensitive_base_field(string $prefix = '') : \stdClass { $fieldname = "{$prefix}sensitivedatareasons"; $data = (object) [ 'fields' => [], ]; $bases = []; foreach (\tool_dataprivacy\purpose::GDPR_ART_9_2_ITEMS as $article) { $key = 'gdpr_art_9_2_' . $article; $bases[$key] = get_string("{$key}_name", 'tool_dataprivacy'); } $data->fields[] = $this->_form->createElement( 'autocomplete', $fieldname, get_string('sensitivedatareasons', 'tool_dataprivacy'), $bases, [ 'multiple' => true, ] ); $data->helps = [ $fieldname => ['sensitivedatareasons', 'tool_dataprivacy'], ]; $data->advanceds = [ $fieldname => true, ]; return $data; } /** * Get the retention period fields. * * @param string $prefix The name of the main field, and prefix for the subfields. * @return \stdClass */ protected function get_retention_period_fields(string $prefix = '') : \stdClass { $prefix = "{$prefix}retentionperiod"; $data = (object) [ 'fields' => [], 'types' => [], ]; $number = $this->_form->createElement('text', "{$prefix}number", null, ['size' => 8]); $data->types["{$prefix}number"] = PARAM_INT; $unitoptions = [ 'Y' => get_string('years'), 'M' => strtolower(get_string('months')), 'D' => strtolower(get_string('days')) ]; $unit = $this->_form->createElement('select', "{$prefix}unit", '', $unitoptions); $data->fields[] = $this->_form->createElement( 'group', $prefix, get_string('retentionperiod', 'tool_dataprivacy'), [ 'number' => $number, 'unit' => $unit, ], null, false ); return $data; } /** * Get the mform field for the protected flag. * * @param string $prefix The prefix to apply to the field * @return \stdClass */ protected function get_protected_field(string $prefix = '') : \stdClass { $fieldname = "{$prefix}protected"; return (object) [ 'fields' => [ $this->_form->createElement( 'advcheckbox', $fieldname, get_string('protected', 'tool_dataprivacy'), get_string('protectedlabel', 'tool_dataprivacy') ), ], ]; } /** * Converts data to data suitable for storage. * * @param \stdClass $data * @return \stdClass */ protected static function convert_fields(\stdClass $data) { $data = parent::convert_fields($data); if (!empty($data->lawfulbases) && is_array($data->lawfulbases)) { $data->lawfulbases = implode(',', $data->lawfulbases); } if (!empty($data->sensitivedatareasons) && is_array($data->sensitivedatareasons)) { $data->sensitivedatareasons = implode(',', $data->sensitivedatareasons); } else { // Nothing selected. Set default value of null. $data->sensitivedatareasons = null; } // A single value. $data->retentionperiod = 'P' . $data->retentionperiodnumber . $data->retentionperiodunit; unset($data->retentionperiodnumber); unset($data->retentionperiodunit); return $data; } /** * Get the default data. * * @return \stdClass */ protected function get_default_data() { $data = parent::get_default_data(); return $this->convert_existing_data_to_values($data); } /** * Normalise any values stored in existing data. * * @param \stdClass $data * @return \stdClass */ protected function convert_existing_data_to_values(\stdClass $data) : \stdClass { $data->lawfulbases = explode(',', $data->lawfulbases); if (!empty($data->sensitivedatareasons)) { $data->sensitivedatareasons = explode(',', $data->sensitivedatareasons); } // Convert the single properties into number and unit. $strlen = strlen($data->retentionperiod); $data->retentionperiodnumber = substr($data->retentionperiod, 1, $strlen - 2); $data->retentionperiodunit = substr($data->retentionperiod, $strlen - 1); unset($data->retentionperiod); return $data; } /** * Fetch the role override data from the list of submitted data. * * @param \stdClass $data The complete set of processed data * @return \stdClass[] The list of overrides */ public function get_role_overrides_from_data(\stdClass $data) { $overrides = []; if (!empty($data->overrides)) { $searchkey = 'roleoverride_'; for ($i = 0; $i < $data->overrides; $i++) { $overridedata = (object) []; foreach ((array) $data as $fieldname => $value) { if (strpos($fieldname, $searchkey) !== 0) { continue; } $overridefieldname = substr($fieldname, strlen($searchkey)); $overridedata->$overridefieldname = $value[$i]; } if (empty($overridedata->roleid) || empty($overridedata->retentionperiodnumber)) { // Skip this one. // There is no value and it will be delete. continue; } $override = static::convert_fields($overridedata); $overrides[$i] = $override; } } return $overrides; } /** * Define extra validation mechanims. * * @param stdClass $data Data to validate. * @param array $files Array of files. * @param array $errors Currently reported errors. * @return array of additional errors, or overridden errors. */ protected function extra_validation($data, $files, array &$errors) { $overrides = $this->get_role_overrides_from_data($data); // Check role overrides to ensure that: // - roles are unique; and // - specifeid retention periods are numeric. $seenroleids = []; foreach ($overrides as $id => $override) { $override->purposeid = 0; $persistent = new \tool_dataprivacy\purpose_override($override->id, $override); if (isset($seenroleids[$persistent->get('roleid')])) { $errors["roleoverride_roleid[{$id}]"] = get_string('duplicaterole'); } $seenroleids[$persistent->get('roleid')] = true; $errors = array_merge($errors, $persistent->get_errors()); } return $errors; } /** * Load in existing data as form defaults. Usually new entry defaults are stored directly in * form definition (new entry form); this function is used to load in data where values * already exist and data is being edited (edit entry form). * * @param stdClass $data */ public function set_data($data) { $purpose = $this->get_persistent(); $count = 0; foreach ($this->existingoverrides as $override) { $overridedata = $this->convert_existing_data_to_values($override->to_record()); foreach ($overridedata as $key => $value) { $keyname = "roleoverride_{$key}[{$count}]"; $data->$keyname = $value; } $count++; } parent::set_data($data); } } classes/form/category.php 0000644 00000004324 15152701722 0011477 0 ustar 00 <?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/>. /** * This file contains the form add/update a data category. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\form; defined('MOODLE_INTERNAL') || die(); use core\form\persistent; /** * Data category form. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class category extends persistent { /** * @var The persistent class. */ protected static $persistentclass = 'tool_dataprivacy\\category'; /** * Define the form - called by parent constructor */ public function definition() { $mform = $this->_form; $mform->addElement('text', 'name', get_string('name'), 'maxlength="100"'); $mform->setType('name', PARAM_TEXT); $mform->addRule('name', get_string('required'), 'required', null, 'server'); $mform->addRule('name', get_string('maximumchars', '', 100), 'maxlength', 100, 'server'); $mform->addElement('editor', 'description', get_string('description'), null, ['autosave' => false]); $mform->setType('description', PARAM_CLEANHTML); if (!empty($this->_customdata['showbuttons'])) { if (!$this->get_persistent()->get('id')) { $savetext = get_string('add'); } else { $savetext = get_string('savechanges'); } $this->add_action_buttons(true, $savetext); } } } classes/form/contextlevel.php 0000644 00000010657 15152701722 0012404 0 ustar 00 <?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/>. /** * This file contains the form add/update context level data. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\form; defined('MOODLE_INTERNAL') || die(); use core\form\persistent; use tool_dataprivacy\api; use tool_dataprivacy\data_registry; /** * Context level data form. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class contextlevel extends context_instance { /** * @var The persistent class. */ protected static $persistentclass = 'tool_dataprivacy\\contextlevel'; /** * Define the form - called by parent constructor */ public function definition() { $this->_form->setDisableShortforms(); $this->_form->addElement('header', 'contextlevelname', $this->_customdata['contextlevelname']); $this->add_purpose_category(); $this->_form->addElement('hidden', 'contextlevel'); $this->_form->setType('contextlevel', PARAM_INT); parent::add_action_buttons(false, get_string('savechanges')); } /** * Returns the customdata array for the provided context level. * * @param int $contextlevel * @return array */ public static function get_contextlevel_customdata($contextlevel) { $persistent = \tool_dataprivacy\contextlevel::get_record_by_contextlevel($contextlevel, false); if (!$persistent) { $persistent = new \tool_dataprivacy\contextlevel(); $persistent->set('contextlevel', $contextlevel); } $includeinherit = true; if ($contextlevel == CONTEXT_SYSTEM) { // Nothing to inherit from Site level. $includeinherit = false; } $includenotset = true; if ($contextlevel == CONTEXT_SYSTEM || $contextlevel == CONTEXT_USER) { // No 'not set' value for system and user because we do not have defaults for them. $includenotset = false; } $purposeoptions = \tool_dataprivacy\output\data_registry_page::purpose_options( api::get_purposes(), $includenotset, $includeinherit); $categoryoptions = \tool_dataprivacy\output\data_registry_page::category_options( api::get_categories(), $includenotset, $includeinherit); $customdata = [ 'contextlevel' => $contextlevel, 'contextlevelname' => get_string('contextlevelname' . $contextlevel, 'tool_dataprivacy'), 'persistent' => $persistent, 'purposes' => $purposeoptions, 'categories' => $categoryoptions, ]; $effectivepurpose = api::get_effective_contextlevel_purpose($contextlevel); if ($effectivepurpose) { $customdata['currentretentionperiod'] = self::get_retention_display_text($effectivepurpose, $contextlevel, \context_system::instance()); $customdata['purposeretentionperiods'] = []; foreach ($purposeoptions as $optionvalue => $unused) { // Get the effective purpose if $optionvalue would be the selected value. list($purposeid, $unused) = data_registry::get_effective_default_contextlevel_purpose_and_category($contextlevel, $optionvalue); $purpose = new \tool_dataprivacy\purpose($purposeid); $retentionperiod = self::get_retention_display_text( $purpose, $contextlevel, \context_system::instance() ); $customdata['purposeretentionperiods'][$optionvalue] = $retentionperiod; } } return $customdata; } } classes/form/contactdpo.php 0000644 00000005541 15152701722 0012022 0 ustar 00 <?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/>. namespace tool_dataprivacy\form; use context; use context_user; use moodle_exception; use moodle_url; use core_form\dynamic_form; use tool_dataprivacy\api; use tool_dataprivacy\external; /** * Contact DPO modal form * * @package tool_dataprivacy * @copyright 2021 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class contactdpo extends dynamic_form { /** * Form definition */ protected function definition() { global $USER; $mform = $this->_form; $mform->addElement('static', 'replyto', get_string('replyto', 'tool_dataprivacy'), s($USER->email)); $mform->addElement('textarea', 'message', get_string('message', 'tool_dataprivacy'), 'cols="60" rows="8"'); $mform->setType('message', PARAM_TEXT); $mform->addRule('message', get_string('required'), 'required', null, 'client'); } /** * Return form context * * @return context */ protected function get_context_for_dynamic_submission(): context { global $USER; return context_user::instance($USER->id); } /** * Check if current user has access to this form, otherwise throw exception * * @throws moodle_exception */ protected function check_access_for_dynamic_submission(): void { if (!api::can_contact_dpo()) { throw new moodle_exception('errorcontactdpodisabled', 'tool_dataprivacy'); } } /** * Process the form submission, used if form was submitted via AJAX * * @return array */ public function process_dynamic_submission() { return external::contact_dpo($this->get_data()->message); } /** * Load in existing data as form defaults (not applicable) */ public function set_data_for_dynamic_submission(): void { return; } /** * Returns url to set in $PAGE->set_url() when form is being rendered or submitted via AJAX * * @return moodle_url */ protected function get_page_url_for_dynamic_submission(): moodle_url { global $USER; return new moodle_url('/user/profile.php', ['id' => $USER->id]); } } classes/external.php 0000644 00000157740 15152701722 0010554 0 ustar 00 <?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/>. /** * Class containing the external API functions functions for the Data Privacy tool. * * @package tool_dataprivacy * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy; defined('MOODLE_INTERNAL') || die(); require_once("$CFG->libdir/externallib.php"); require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php'); use coding_exception; use context_helper; use context_system; use context_user; use core\invalid_persistent_exception; use core\notification; use core_user; use dml_exception; use external_api; use external_description; use external_function_parameters; use external_multiple_structure; use external_single_structure; use external_value; use external_warnings; use invalid_parameter_exception; use moodle_exception; use required_capability_exception; use restricted_context_exception; use tool_dataprivacy\external\category_exporter; use tool_dataprivacy\external\data_request_exporter; use tool_dataprivacy\external\purpose_exporter; use tool_dataprivacy\output\data_registry_page; /** * Class external. * * The external API for the Data Privacy tool. * * @copyright 2017 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class external extends external_api { /** * Parameter description for cancel_data_request(). * * @since Moodle 3.5 * @return external_function_parameters */ public static function cancel_data_request_parameters() { return new external_function_parameters([ 'requestid' => new external_value(PARAM_INT, 'The request ID', VALUE_REQUIRED) ]); } /** * Cancel a data request. * * @since Moodle 3.5 * @param int $requestid The request ID. * @return array * @throws invalid_persistent_exception * @throws coding_exception * @throws invalid_parameter_exception * @throws restricted_context_exception */ public static function cancel_data_request($requestid) { global $USER; $warnings = []; $params = external_api::validate_parameters(self::cancel_data_request_parameters(), [ 'requestid' => $requestid ]); $requestid = $params['requestid']; // Validate context and access to manage the registry. $context = context_user::instance($USER->id); self::validate_context($context); // Ensure the request exists. $select = 'id = :id AND (userid = :userid OR requestedby = :requestedby)'; $params = ['id' => $requestid, 'userid' => $USER->id, 'requestedby' => $USER->id]; $requests = data_request::get_records_select($select, $params); $requestexists = count($requests) === 1; $result = false; if ($requestexists) { $request = reset($requests); $datasubject = $request->get('userid'); if ($datasubject !== (int) $USER->id) { // The user is not the subject. Check that they can cancel this request. if (!api::can_create_data_request_for_user($datasubject)) { $forusercontext = \context_user::instance($datasubject); throw new required_capability_exception($forusercontext, 'tool/dataprivacy:makedatarequestsforchildren', 'nopermissions', ''); } } // TODO: Do we want a request to be non-cancellable past a certain point? E.g. When it's already approved/processing. $result = api::update_request_status($requestid, api::DATAREQUEST_STATUS_CANCELLED); } else { $warnings[] = [ 'item' => $requestid, 'warningcode' => 'errorrequestnotfound', 'message' => get_string('errorrequestnotfound', 'tool_dataprivacy') ]; } return [ 'result' => $result, 'warnings' => $warnings ]; } /** * Parameter description for cancel_data_request(). * * @since Moodle 3.5 * @return external_description */ public static function cancel_data_request_returns() { return new external_single_structure([ 'result' => new external_value(PARAM_BOOL, 'The processing result'), 'warnings' => new external_warnings() ]); } /** * Parameter description for contact_dpo(). * * @since Moodle 3.5 * @return external_function_parameters */ public static function contact_dpo_parameters() { return new external_function_parameters([ 'message' => new external_value(PARAM_TEXT, 'The user\'s message to the Data Protection Officer(s)', VALUE_REQUIRED) ]); } /** * Make a general enquiry to a DPO. * * @since Moodle 3.5 * @param string $message The message to be sent to the DPO. * @return array * @throws coding_exception * @throws invalid_parameter_exception * @throws invalid_persistent_exception * @throws restricted_context_exception * @throws dml_exception * @throws moodle_exception */ public static function contact_dpo($message) { global $USER; $warnings = []; $params = external_api::validate_parameters(self::contact_dpo_parameters(), [ 'message' => $message ]); $message = $params['message']; // Validate context. $userid = $USER->id; $context = context_user::instance($userid); self::validate_context($context); // Lodge the request. $datarequest = new data_request(); // The user the request is being made for. $datarequest->set('userid', $userid); // The user making the request. $datarequest->set('requestedby', $userid); // Set status. $datarequest->set('status', api::DATAREQUEST_STATUS_PENDING); // Set request type. $datarequest->set('type', api::DATAREQUEST_TYPE_OTHERS); // Set request comments. $datarequest->set('comments', $message); // Store subject access request. $datarequest->create(); // Get the list of the site Data Protection Officers. $dpos = api::get_site_dpos(); // Email the data request to the Data Protection Officer(s)/Admin(s). $result = true; foreach ($dpos as $dpo) { $sendresult = api::notify_dpo($dpo, $datarequest); if (!$sendresult) { $result = false; $warnings[] = [ 'item' => $dpo->id, 'warningcode' => 'errorsendingtodpo', 'message' => get_string('errorsendingmessagetodpo', 'tool_dataprivacy', fullname($dpo)) ]; } } return [ 'result' => $result, 'warnings' => $warnings ]; } /** * Parameter description for contact_dpo(). * * @since Moodle 3.5 * @return external_description */ public static function contact_dpo_returns() { return new external_single_structure([ 'result' => new external_value(PARAM_BOOL, 'The processing result'), 'warnings' => new external_warnings() ]); } /** * Parameter description for mark_complete(). * * @since Moodle 3.5.2 * @return external_function_parameters */ public static function mark_complete_parameters() { return new external_function_parameters([ 'requestid' => new external_value(PARAM_INT, 'The request ID', VALUE_REQUIRED) ]); } /** * Mark a user's general enquiry's status as complete. * * @since Moodle 3.5.2 * @param int $requestid The request ID of the general enquiry. * @return array * @throws coding_exception * @throws invalid_parameter_exception * @throws invalid_persistent_exception * @throws restricted_context_exception * @throws dml_exception * @throws moodle_exception */ public static function mark_complete($requestid) { global $USER; $warnings = []; $params = external_api::validate_parameters(self::mark_complete_parameters(), [ 'requestid' => $requestid, ]); $requestid = $params['requestid']; // Validate context and access to manage the registry. $context = context_system::instance(); self::validate_context($context); api::check_can_manage_data_registry(); $message = get_string('markedcomplete', 'tool_dataprivacy'); // Update the data request record. if ($result = api::update_request_status($requestid, api::DATAREQUEST_STATUS_COMPLETE, $USER->id, $message)) { // Add notification in the session to be shown when the page is reloaded on the JS side. notification::success(get_string('requestmarkedcomplete', 'tool_dataprivacy')); } return [ 'result' => $result, 'warnings' => $warnings ]; } /** * Parameter description for mark_complete(). * * @since Moodle 3.5.2 * @return external_description */ public static function mark_complete_returns() { return new external_single_structure([ 'result' => new external_value(PARAM_BOOL, 'The processing result'), 'warnings' => new external_warnings() ]); } /** * Parameter description for get_data_request(). * * @since Moodle 3.5 * @return external_function_parameters */ public static function get_data_request_parameters() { return new external_function_parameters([ 'requestid' => new external_value(PARAM_INT, 'The request ID', VALUE_REQUIRED) ]); } /** * Fetch the details of a user's data request. * * @since Moodle 3.5 * @param int $requestid The request ID. * @return array * @throws coding_exception * @throws dml_exception * @throws invalid_parameter_exception * @throws restricted_context_exception * @throws moodle_exception */ public static function get_data_request($requestid) { global $PAGE; $warnings = []; $params = external_api::validate_parameters(self::get_data_request_parameters(), [ 'requestid' => $requestid ]); $requestid = $params['requestid']; // Validate context. $context = context_system::instance(); self::validate_context($context); $requestpersistent = new data_request($requestid); require_capability('tool/dataprivacy:managedatarequests', $context); $exporter = new data_request_exporter($requestpersistent, ['context' => $context]); $renderer = $PAGE->get_renderer('tool_dataprivacy'); $result = $exporter->export($renderer); return [ 'result' => $result, 'warnings' => $warnings ]; } /** * Parameter description for get_data_request(). * * @since Moodle 3.5 * @return external_description */ public static function get_data_request_returns() { return new external_single_structure([ 'result' => data_request_exporter::get_read_structure(), 'warnings' => new external_warnings() ]); } /** * Parameter description for approve_data_request(). * * @since Moodle 3.5 * @return external_function_parameters */ public static function approve_data_request_parameters() { return new external_function_parameters([ 'requestid' => new external_value(PARAM_INT, 'The request ID', VALUE_REQUIRED) ]); } /** * Approve a data request. * * @since Moodle 3.5 * @param int $requestid The request ID. * @return array * @throws coding_exception * @throws dml_exception * @throws invalid_parameter_exception * @throws restricted_context_exception * @throws moodle_exception */ public static function approve_data_request($requestid) { $warnings = []; $params = external_api::validate_parameters(self::approve_data_request_parameters(), [ 'requestid' => $requestid ]); $requestid = $params['requestid']; // Validate context. $context = context_system::instance(); self::validate_context($context); require_capability('tool/dataprivacy:managedatarequests', $context); // Ensure the request exists. $requestexists = data_request::record_exists($requestid); $result = false; if ($requestexists) { $result = api::approve_data_request($requestid); // Add notification in the session to be shown when the page is reloaded on the JS side. notification::success(get_string('requestapproved', 'tool_dataprivacy')); } else { $warnings[] = [ 'item' => $requestid, 'warningcode' => 'errorrequestnotfound', 'message' => get_string('errorrequestnotfound', 'tool_dataprivacy') ]; } return [ 'result' => $result, 'warnings' => $warnings ]; } /** * Parameter description for approve_data_request(). * * @since Moodle 3.5 * @return external_description */ public static function approve_data_request_returns() { return new external_single_structure([ 'result' => new external_value(PARAM_BOOL, 'The processing result'), 'warnings' => new external_warnings() ]); } /** * Parameter description for bulk_approve_data_requests(). * * @since Moodle 3.5 * @return external_function_parameters */ public static function bulk_approve_data_requests_parameters() { return new external_function_parameters([ 'requestids' => new external_multiple_structure( new external_value(PARAM_INT, 'The request ID', VALUE_REQUIRED) ) ]); } /** * Bulk approve bulk data request. * * @since Moodle 3.5 * @param array $requestids Array consisting the request ID's. * @return array * @throws coding_exception * @throws dml_exception * @throws invalid_parameter_exception * @throws restricted_context_exception * @throws moodle_exception */ public static function bulk_approve_data_requests($requestids) { $warnings = []; $result = false; $params = external_api::validate_parameters(self::bulk_approve_data_requests_parameters(), [ 'requestids' => $requestids ]); $requestids = $params['requestids']; // Validate context. $context = context_system::instance(); self::validate_context($context); require_capability('tool/dataprivacy:managedatarequests', $context); foreach ($requestids as $requestid) { // Ensure the request exists. $requestexists = data_request::record_exists($requestid); if ($requestexists) { api::approve_data_request($requestid); } else { $warnings[] = [ 'item' => $requestid, 'warningcode' => 'errorrequestnotfound', 'message' => get_string('errorrequestnotfound', 'tool_dataprivacy') ]; } } if (empty($warnings)) { $result = true; // Add notification in the session to be shown when the page is reloaded on the JS side. notification::success(get_string('requestsapproved', 'tool_dataprivacy')); } return [ 'result' => $result, 'warnings' => $warnings ]; } /** * Parameter description for bulk_approve_data_requests(). * * @since Moodle 3.5 * @return external_description */ public static function bulk_approve_data_requests_returns() { return new external_single_structure([ 'result' => new external_value(PARAM_BOOL, 'The processing result'), 'warnings' => new external_warnings() ]); } /** * Parameter description for deny_data_request(). * * @since Moodle 3.5 * @return external_function_parameters */ public static function deny_data_request_parameters() { return new external_function_parameters([ 'requestid' => new external_value(PARAM_INT, 'The request ID', VALUE_REQUIRED) ]); } /** * Deny a data request. * * @since Moodle 3.5 * @param int $requestid The request ID. * @return array * @throws coding_exception * @throws dml_exception * @throws invalid_parameter_exception * @throws restricted_context_exception * @throws moodle_exception */ public static function deny_data_request($requestid) { $warnings = []; $params = external_api::validate_parameters(self::deny_data_request_parameters(), [ 'requestid' => $requestid ]); $requestid = $params['requestid']; // Validate context. $context = context_system::instance(); self::validate_context($context); require_capability('tool/dataprivacy:managedatarequests', $context); // Ensure the request exists. $requestexists = data_request::record_exists($requestid); $result = false; if ($requestexists) { $result = api::deny_data_request($requestid); // Add notification in the session to be shown when the page is reloaded on the JS side. notification::success(get_string('requestdenied', 'tool_dataprivacy')); } else { $warnings[] = [ 'item' => $requestid, 'warningcode' => 'errorrequestnotfound', 'message' => get_string('errorrequestnotfound', 'tool_dataprivacy') ]; } return [ 'result' => $result, 'warnings' => $warnings ]; } /** * Parameter description for deny_data_request(). * * @since Moodle 3.5 * @return external_description */ public static function deny_data_request_returns() { return new external_single_structure([ 'result' => new external_value(PARAM_BOOL, 'The processing result'), 'warnings' => new external_warnings() ]); } /** * Parameter description for bulk_deny_data_requests(). * * @since Moodle 3.5 * @return external_function_parameters */ public static function bulk_deny_data_requests_parameters() { return new external_function_parameters([ 'requestids' => new external_multiple_structure( new external_value(PARAM_INT, 'The request ID', VALUE_REQUIRED) ) ]); } /** * Bulk deny data requests. * * @since Moodle 3.5 * @param array $requestids Array consisting of request ID's. * @return array * @throws coding_exception * @throws dml_exception * @throws invalid_parameter_exception * @throws restricted_context_exception * @throws moodle_exception */ public static function bulk_deny_data_requests($requestids) { $warnings = []; $result = false; $params = external_api::validate_parameters(self::bulk_deny_data_requests_parameters(), [ 'requestids' => $requestids ]); $requestids = $params['requestids']; // Validate context. $context = context_system::instance(); self::validate_context($context); require_capability('tool/dataprivacy:managedatarequests', $context); foreach ($requestids as $requestid) { // Ensure the request exists. $requestexists = data_request::record_exists($requestid); if ($requestexists) { api::deny_data_request($requestid); } else { $warnings[] = [ 'item' => $requestid, 'warningcode' => 'errorrequestnotfound', 'message' => get_string('errorrequestnotfound', 'tool_dataprivacy') ]; } } if (empty($warnings)) { $result = true; // Add notification in the session to be shown when the page is reloaded on the JS side. notification::success(get_string('requestsdenied', 'tool_dataprivacy')); } return [ 'result' => $result, 'warnings' => $warnings ]; } /** * Parameter description for bulk_deny_data_requests(). * * @since Moodle 3.5 * @return external_description */ public static function bulk_deny_data_requests_returns() { return new external_single_structure([ 'result' => new external_value(PARAM_BOOL, 'The processing result'), 'warnings' => new external_warnings() ]); } /** * Parameter description for get_data_request(). * * @since Moodle 3.5 * @return external_function_parameters */ public static function get_users_parameters() { return new external_function_parameters([ 'query' => new external_value(PARAM_TEXT, 'The search query', VALUE_REQUIRED) ]); } /** * Fetch the details of a user's data request. * * @since Moodle 3.5 * @param string $query The search request. * @return array * @throws required_capability_exception * @throws dml_exception * @throws invalid_parameter_exception * @throws restricted_context_exception */ public static function get_users($query) { global $DB; $params = external_api::validate_parameters(self::get_users_parameters(), [ 'query' => $query ]); $query = $params['query']; // Validate context. $context = context_system::instance(); self::validate_context($context); require_capability('tool/dataprivacy:managedatarequests', $context); $userfieldsapi = \core_user\fields::for_name(); $allusernames = $userfieldsapi->get_sql('', false, '', '', false)->selects; // Exclude admins and guest user. $excludedusers = array_keys(get_admins()) + [guest_user()->id]; $sort = 'lastname ASC, firstname ASC'; $fields = 'id,' . $allusernames; // TODO Does not support custom user profile fields (MDL-70456). $extrafields = \core_user\fields::get_identity_fields($context, false); if (!empty($extrafields)) { $fields .= ',' . implode(',', $extrafields); } list($sql, $params) = users_search_sql($query, '', false, $extrafields, $excludedusers); $users = $DB->get_records_select('user', $sql, $params, $sort, $fields, 0, 30); $useroptions = []; foreach ($users as $user) { $useroption = (object)[ 'id' => $user->id, 'fullname' => fullname($user) ]; $useroption->extrafields = []; foreach ($extrafields as $extrafield) { // Sanitize the extra fields to prevent potential XSS exploit. $useroption->extrafields[] = (object)[ 'name' => $extrafield, 'value' => s($user->$extrafield) ]; } $useroptions[$user->id] = $useroption; } return $useroptions; } /** * Parameter description for get_users(). * * @since Moodle 3.5 * @return external_description * @throws coding_exception */ public static function get_users_returns() { return new external_multiple_structure(new external_single_structure( [ 'id' => new external_value(core_user::get_property_type('id'), 'ID of the user'), 'fullname' => new external_value(core_user::get_property_type('firstname'), 'The fullname of the user'), 'extrafields' => new external_multiple_structure( new external_single_structure([ 'name' => new external_value(PARAM_TEXT, 'Name of the extrafield.'), 'value' => new external_value(PARAM_TEXT, 'Value of the extrafield.') ] ), 'List of extra fields', VALUE_OPTIONAL ) ] )); } /** * Parameter description for create_purpose_form(). * * @since Moodle 3.5 * @return external_function_parameters */ public static function create_purpose_form_parameters() { return new external_function_parameters([ 'jsonformdata' => new external_value(PARAM_RAW, 'The data to create the purpose, encoded as a json array') ]); } /** * Creates a data purpose from form data. * * @since Moodle 3.5 * @param string $jsonformdata * @return array */ public static function create_purpose_form($jsonformdata) { global $PAGE; $warnings = []; $params = external_api::validate_parameters(self::create_purpose_form_parameters(), [ 'jsonformdata' => $jsonformdata ]); // Validate context and access to manage the registry. self::validate_context(\context_system::instance()); api::check_can_manage_data_registry(); $serialiseddata = json_decode($params['jsonformdata']); $data = array(); parse_str($serialiseddata, $data); $purpose = new \tool_dataprivacy\purpose(0); $mform = new \tool_dataprivacy\form\purpose(null, ['persistent' => $purpose], 'post', '', null, true, $data); $validationerrors = true; if ($validateddata = $mform->get_data()) { $purpose = api::create_purpose($validateddata); $validationerrors = false; } else if ($errors = $mform->is_validated()) { throw new moodle_exception('generalerror'); } $exporter = new purpose_exporter($purpose, ['context' => \context_system::instance()]); return [ 'purpose' => $exporter->export($PAGE->get_renderer('core')), 'validationerrors' => $validationerrors, 'warnings' => $warnings ]; } /** * Returns for create_purpose_form(). * * @since Moodle 3.5 * @return external_single_structure */ public static function create_purpose_form_returns() { return new external_single_structure([ 'purpose' => purpose_exporter::get_read_structure(), 'validationerrors' => new external_value(PARAM_BOOL, 'Were there validation errors', VALUE_REQUIRED), 'warnings' => new external_warnings() ]); } /** * Parameter description for delete_purpose(). * * @since Moodle 3.5 * @return external_function_parameters */ public static function delete_purpose_parameters() { return new external_function_parameters([ 'id' => new external_value(PARAM_INT, 'The purpose ID', VALUE_REQUIRED) ]); } /** * Deletes a data purpose. * * @since Moodle 3.5 * @param int $id The ID. * @return array * @throws invalid_persistent_exception * @throws coding_exception * @throws invalid_parameter_exception */ public static function delete_purpose($id) { global $USER; $params = external_api::validate_parameters(self::delete_purpose_parameters(), [ 'id' => $id ]); // Validate context and access to manage the registry. self::validate_context(\context_system::instance()); api::check_can_manage_data_registry(); $result = api::delete_purpose($params['id']); return [ 'result' => $result, 'warnings' => [] ]; } /** * Parameter description for delete_purpose(). * * @since Moodle 3.5 * @return external_single_structure */ public static function delete_purpose_returns() { return new external_single_structure([ 'result' => new external_value(PARAM_BOOL, 'The processing result'), 'warnings' => new external_warnings() ]); } /** * Parameter description for create_category_form(). * * @since Moodle 3.5 * @return external_function_parameters */ public static function create_category_form_parameters() { return new external_function_parameters([ 'jsonformdata' => new external_value(PARAM_RAW, 'The data to create the category, encoded as a json array') ]); } /** * Creates a data category from form data. * * @since Moodle 3.5 * @param string $jsonformdata * @return array */ public static function create_category_form($jsonformdata) { global $PAGE; $warnings = []; $params = external_api::validate_parameters(self::create_category_form_parameters(), [ 'jsonformdata' => $jsonformdata ]); // Validate context and access to manage the registry. self::validate_context(\context_system::instance()); api::check_can_manage_data_registry(); $serialiseddata = json_decode($params['jsonformdata']); $data = array(); parse_str($serialiseddata, $data); $category = new \tool_dataprivacy\category(0); $mform = new \tool_dataprivacy\form\category(null, ['persistent' => $category], 'post', '', null, true, $data); $validationerrors = true; if ($validateddata = $mform->get_data()) { $category = api::create_category($validateddata); $validationerrors = false; } else if ($errors = $mform->is_validated()) { throw new moodle_exception('generalerror'); } $exporter = new category_exporter($category, ['context' => \context_system::instance()]); return [ 'category' => $exporter->export($PAGE->get_renderer('core')), 'validationerrors' => $validationerrors, 'warnings' => $warnings ]; } /** * Returns for create_category_form(). * * @since Moodle 3.5 * @return external_single_structure */ public static function create_category_form_returns() { return new external_single_structure([ 'category' => category_exporter::get_read_structure(), 'validationerrors' => new external_value(PARAM_BOOL, 'Were there validation errors', VALUE_REQUIRED), 'warnings' => new external_warnings() ]); } /** * Parameter description for delete_category(). * * @since Moodle 3.5 * @return external_function_parameters */ public static function delete_category_parameters() { return new external_function_parameters([ 'id' => new external_value(PARAM_INT, 'The category ID', VALUE_REQUIRED) ]); } /** * Deletes a data category. * * @since Moodle 3.5 * @param int $id The ID. * @return array * @throws invalid_persistent_exception * @throws coding_exception * @throws invalid_parameter_exception */ public static function delete_category($id) { global $USER; $params = external_api::validate_parameters(self::delete_category_parameters(), [ 'id' => $id ]); // Validate context and access to manage the registry. self::validate_context(\context_system::instance()); api::check_can_manage_data_registry(); $result = api::delete_category($params['id']); return [ 'result' => $result, 'warnings' => [] ]; } /** * Parameter description for delete_category(). * * @since Moodle 3.5 * @return external_single_structure */ public static function delete_category_returns() { return new external_single_structure([ 'result' => new external_value(PARAM_BOOL, 'The processing result'), 'warnings' => new external_warnings() ]); } /** * Parameter description for set_contextlevel_form(). * * @since Moodle 3.5 * @return external_function_parameters */ public static function set_contextlevel_form_parameters() { return new external_function_parameters([ 'jsonformdata' => new external_value(PARAM_RAW, 'The context level data, encoded as a json array') ]); } /** * Creates a data category from form data. * * @since Moodle 3.5 * @param string $jsonformdata * @return array */ public static function set_contextlevel_form($jsonformdata) { global $PAGE; $warnings = []; $params = external_api::validate_parameters(self::set_contextlevel_form_parameters(), [ 'jsonformdata' => $jsonformdata ]); // Validate context and access to manage the registry. self::validate_context(\context_system::instance()); api::check_can_manage_data_registry(); $serialiseddata = json_decode($params['jsonformdata']); $data = array(); parse_str($serialiseddata, $data); $contextlevel = $data['contextlevel']; $customdata = \tool_dataprivacy\form\contextlevel::get_contextlevel_customdata($contextlevel); $mform = new \tool_dataprivacy\form\contextlevel(null, $customdata, 'post', '', null, true, $data); if ($validateddata = $mform->get_data()) { $contextlevel = api::set_contextlevel($validateddata); } else if ($errors = $mform->is_validated()) { $warnings[] = json_encode($errors); } if ($contextlevel) { $result = true; } else { $result = false; } return [ 'result' => $result, 'warnings' => $warnings ]; } /** * Returns for set_contextlevel_form(). * * @since Moodle 3.5 * @return external_single_structure */ public static function set_contextlevel_form_returns() { return new external_single_structure([ 'result' => new external_value(PARAM_BOOL, 'Whether the data was properly set or not'), 'warnings' => new external_warnings() ]); } /** * Parameter description for set_context_form(). * * @since Moodle 3.5 * @return external_function_parameters */ public static function set_context_form_parameters() { return new external_function_parameters([ 'jsonformdata' => new external_value(PARAM_RAW, 'The context level data, encoded as a json array') ]); } /** * Creates a data category from form data. * * @since Moodle 3.5 * @param string $jsonformdata * @return array */ public static function set_context_form($jsonformdata) { global $PAGE; $warnings = []; $params = external_api::validate_parameters(self::set_context_form_parameters(), [ 'jsonformdata' => $jsonformdata ]); // Validate context and access to manage the registry. self::validate_context(\context_system::instance()); api::check_can_manage_data_registry(); $serialiseddata = json_decode($params['jsonformdata']); $data = array(); parse_str($serialiseddata, $data); $context = context_helper::instance_by_id($data['contextid']); $customdata = \tool_dataprivacy\form\context_instance::get_context_instance_customdata($context); $mform = new \tool_dataprivacy\form\context_instance(null, $customdata, 'post', '', null, true, $data); if ($validateddata = $mform->get_data()) { api::check_can_manage_data_registry($validateddata->contextid); $context = api::set_context_instance($validateddata); } else if ($errors = $mform->is_validated()) { $warnings[] = json_encode($errors); throw new moodle_exception('generalerror'); } if ($context) { $result = true; } else { $result = false; } return [ 'result' => $result, 'warnings' => $warnings ]; } /** * Returns for set_context_form(). * * @since Moodle 3.5 * @return external_single_structure */ public static function set_context_form_returns() { return new external_single_structure([ 'result' => new external_value(PARAM_BOOL, 'Whether the data was properly set or not'), 'warnings' => new external_warnings() ]); } /** * Parameter description for tree_extra_branches(). * * @since Moodle 3.5 * @return external_function_parameters */ public static function tree_extra_branches_parameters() { return new external_function_parameters([ 'contextid' => new external_value(PARAM_INT, 'The context id to expand'), 'element' => new external_value(PARAM_ALPHA, 'The element we are interested on') ]); } /** * Returns tree extra branches. * * @since Moodle 3.5 * @param int $contextid * @param string $element * @return array */ public static function tree_extra_branches($contextid, $element) { $params = external_api::validate_parameters(self::tree_extra_branches_parameters(), [ 'contextid' => $contextid, 'element' => $element, ]); $context = context_helper::instance_by_id($params['contextid']); self::validate_context($context); api::check_can_manage_data_registry($context->id); switch ($params['element']) { case 'course': $branches = data_registry_page::get_courses_branch($context); break; case 'module': $branches = data_registry_page::get_modules_branch($context); break; case 'block': $branches = data_registry_page::get_blocks_branch($context); break; default: throw new \moodle_exception('Unsupported element provided.'); } return [ 'branches' => $branches, 'warnings' => [], ]; } /** * Returns for tree_extra_branches(). * * @since Moodle 3.5 * @return external_single_structure */ public static function tree_extra_branches_returns() { return new external_single_structure([ 'branches' => new external_multiple_structure(self::get_tree_node_structure(true)), 'warnings' => new external_warnings() ]); } /** * Parameters for confirm_contexts_for_deletion(). * * @since Moodle 3.5 * @return external_function_parameters */ public static function confirm_contexts_for_deletion_parameters() { return new external_function_parameters([ 'ids' => new external_multiple_structure( new external_value(PARAM_INT, 'Expired context record ID', VALUE_REQUIRED), 'Array of expired context record IDs', VALUE_DEFAULT, [] ), ]); } /** * Confirm a given array of expired context record IDs * * @since Moodle 3.5 * @param int[] $ids Array of record IDs from the expired contexts table. * @return array * @throws coding_exception * @throws dml_exception * @throws invalid_parameter_exception * @throws restricted_context_exception */ public static function confirm_contexts_for_deletion($ids) { $warnings = []; $params = external_api::validate_parameters(self::confirm_contexts_for_deletion_parameters(), [ 'ids' => $ids ]); $ids = $params['ids']; // Validate context and access to manage the registry. self::validate_context(\context_system::instance()); api::check_can_manage_data_registry(); $result = true; if (!empty($ids)) { $expiredcontextstoapprove = []; // Loop through the deletion of expired contexts and their children if necessary. foreach ($ids as $id) { $expiredcontext = new expired_context($id); $targetcontext = context_helper::instance_by_id($expiredcontext->get('contextid')); if (!$targetcontext instanceof \context_user) { // Fetch this context's child contexts. Make sure that all of the child contexts are flagged for deletion. // User context children do not need to be considered. $childcontexts = $targetcontext->get_child_contexts(); foreach ($childcontexts as $child) { if ($expiredchildcontext = expired_context::get_record(['contextid' => $child->id])) { // Add this child context to the list for approval. $expiredcontextstoapprove[] = $expiredchildcontext; } else { // This context has not yet been flagged for deletion. $result = false; $message = get_string('errorcontexthasunexpiredchildren', 'tool_dataprivacy', $targetcontext->get_context_name(false)); $warnings[] = [ 'item' => 'tool_dataprivacy_ctxexpired', 'warningcode' => 'errorcontexthasunexpiredchildren', 'message' => $message ]; // Exit the process. break 2; } } } $expiredcontextstoapprove[] = $expiredcontext; } // Proceed with the approval if everything's in order. if ($result) { // Mark expired contexts as approved for deletion. foreach ($expiredcontextstoapprove as $expired) { // Only mark expired contexts that are pending approval. if ($expired->get('status') == expired_context::STATUS_EXPIRED) { api::set_expired_context_status($expired, expired_context::STATUS_APPROVED); } } } } else { // We don't have anything to process. $result = false; $warnings[] = [ 'item' => 'tool_dataprivacy_ctxexpired', 'warningcode' => 'errornoexpiredcontexts', 'message' => get_string('errornoexpiredcontexts', 'tool_dataprivacy') ]; } return [ 'result' => $result, 'warnings' => $warnings ]; } /** * Returns for confirm_contexts_for_deletion(). * * @since Moodle 3.5 * @return external_single_structure */ public static function confirm_contexts_for_deletion_returns() { return new external_single_structure([ 'result' => new external_value(PARAM_BOOL, 'Whether the record was properly marked for deletion or not'), 'warnings' => new external_warnings() ]); } /** * Parameters for set_context_defaults(). * * @return external_function_parameters */ public static function set_context_defaults_parameters() { return new external_function_parameters([ 'contextlevel' => new external_value(PARAM_INT, 'The context level', VALUE_REQUIRED), 'category' => new external_value(PARAM_INT, 'The default category for the given context level', VALUE_REQUIRED), 'purpose' => new external_value(PARAM_INT, 'The default purpose for the given context level', VALUE_REQUIRED), 'activity' => new external_value(PARAM_PLUGIN, 'The plugin name of the activity', VALUE_DEFAULT, null), 'override' => new external_value(PARAM_BOOL, 'Whether to override existing instances with the defaults', VALUE_DEFAULT, false), ]); } /** * Updates the default category and purpose for a given context level (and optionally, a plugin). * * @param int $contextlevel The context level. * @param int $category The ID matching the category. * @param int $purpose The ID matching the purpose record. * @param int $activity The name of the activity that we're making a defaults configuration for. * @param bool $override Whether to override the purpose/categories of existing instances to these defaults. * @return array */ public static function set_context_defaults($contextlevel, $category, $purpose, $activity, $override) { $warnings = []; $params = external_api::validate_parameters(self::set_context_defaults_parameters(), [ 'contextlevel' => $contextlevel, 'category' => $category, 'purpose' => $purpose, 'activity' => $activity, 'override' => $override, ]); $contextlevel = $params['contextlevel']; $category = $params['category']; $purpose = $params['purpose']; $activity = $params['activity']; $override = $params['override']; // Validate context. $context = context_system::instance(); self::validate_context($context); api::check_can_manage_data_registry(); // Set the context defaults. $result = api::set_context_defaults($contextlevel, $category, $purpose, $activity, $override); return [ 'result' => $result, 'warnings' => $warnings ]; } /** * Returns for set_context_defaults(). * * @return external_single_structure */ public static function set_context_defaults_returns() { return new external_single_structure([ 'result' => new external_value(PARAM_BOOL, 'Whether the context defaults were successfully set or not'), 'warnings' => new external_warnings() ]); } /** * Parameters for get_category_options(). * * @return external_function_parameters */ public static function get_category_options_parameters() { return new external_function_parameters([ 'includeinherit' => new external_value(PARAM_BOOL, 'Include option "Inherit"', VALUE_DEFAULT, true), 'includenotset' => new external_value(PARAM_BOOL, 'Include option "Not set"', VALUE_DEFAULT, false), ]); } /** * Fetches a list of data category options containing category IDs as keys and the category name for the value. * * @param bool $includeinherit Whether to include the "Inherit" option. * @param bool $includenotset Whether to include the "Not set" option. * @return array */ public static function get_category_options($includeinherit, $includenotset) { $warnings = []; $params = self::validate_parameters(self::get_category_options_parameters(), [ 'includeinherit' => $includeinherit, 'includenotset' => $includenotset ]); $includeinherit = $params['includeinherit']; $includenotset = $params['includenotset']; $context = context_system::instance(); self::validate_context($context); api::check_can_manage_data_registry(); $categories = api::get_categories(); $options = data_registry_page::category_options($categories, $includenotset, $includeinherit); $categoryoptions = []; foreach ($options as $id => $name) { $categoryoptions[] = [ 'id' => $id, 'name' => $name, ]; } return [ 'options' => $categoryoptions, 'warnings' => $warnings ]; } /** * Returns for get_category_options(). * * @return external_single_structure */ public static function get_category_options_returns() { $optiondefinition = new external_single_structure( [ 'id' => new external_value(PARAM_INT, 'The category ID'), 'name' => new external_value(PARAM_TEXT, 'The category name'), ] ); return new external_single_structure([ 'options' => new external_multiple_structure($optiondefinition), 'warnings' => new external_warnings() ]); } /** * Parameters for get_purpose_options(). * * @return external_function_parameters */ public static function get_purpose_options_parameters() { return new external_function_parameters([ 'includeinherit' => new external_value(PARAM_BOOL, 'Include option "Inherit"', VALUE_DEFAULT, true), 'includenotset' => new external_value(PARAM_BOOL, 'Include option "Not set"', VALUE_DEFAULT, false), ]); } /** * Fetches a list of data storage purposes containing purpose IDs as keys and the purpose name for the value. * * @param bool $includeinherit Whether to include the "Inherit" option. * @param bool $includenotset Whether to include the "Not set" option. * @return array */ public static function get_purpose_options($includeinherit, $includenotset) { $warnings = []; $params = self::validate_parameters(self::get_category_options_parameters(), [ 'includeinherit' => $includeinherit, 'includenotset' => $includenotset ]); $includeinherit = $params['includeinherit']; $includenotset = $params['includenotset']; $context = context_system::instance(); self::validate_context($context); $purposes = api::get_purposes(); $options = data_registry_page::purpose_options($purposes, $includenotset, $includeinherit); $purposeoptions = []; foreach ($options as $id => $name) { $purposeoptions[] = [ 'id' => $id, 'name' => $name, ]; } return [ 'options' => $purposeoptions, 'warnings' => $warnings ]; } /** * Returns for get_purpose_options(). * * @return external_single_structure */ public static function get_purpose_options_returns() { $optiondefinition = new external_single_structure( [ 'id' => new external_value(PARAM_INT, 'The purpose ID'), 'name' => new external_value(PARAM_TEXT, 'The purpose name'), ] ); return new external_single_structure([ 'options' => new external_multiple_structure($optiondefinition), 'warnings' => new external_warnings() ]); } /** * Parameters for get_activity_options(). * * @return external_function_parameters */ public static function get_activity_options_parameters() { return new external_function_parameters([ 'nodefaults' => new external_value(PARAM_BOOL, 'Whether to fetch all activities or only those without defaults', VALUE_DEFAULT, false), ]); } /** * Fetches a list of activity options for setting data registry defaults. * * @param boolean $nodefaults If false, it will fetch all of the activities. Otherwise, it will only fetch the activities * that don't have defaults yet (e.g. when adding a new activity module defaults). * @return array */ public static function get_activity_options($nodefaults) { $warnings = []; $params = self::validate_parameters(self::get_activity_options_parameters(), [ 'nodefaults' => $nodefaults, ]); $nodefaults = $params['nodefaults']; $context = context_system::instance(); self::validate_context($context); // Get activity module plugin info. $pluginmanager = \core_plugin_manager::instance(); $modplugins = $pluginmanager->get_enabled_plugins('mod'); $modoptions = []; // Get the module-level defaults. data_registry::get_defaults falls back to this when there are no activity defaults. list($levelpurpose, $levelcategory) = data_registry::get_defaults(CONTEXT_MODULE); foreach ($modplugins as $name) { // Check if we have default purpose and category for this module if we want don't want to fetch everything. if ($nodefaults) { list($purpose, $category) = data_registry::get_defaults(CONTEXT_MODULE, $name); // Compare this with the module-level defaults. if ($purpose !== $levelpurpose || $category !== $levelcategory) { // If the defaults for this activity has been already set, there's no need to add this in the list of options. continue; } } $displayname = $pluginmanager->plugin_name('mod_' . $name); $modoptions[] = (object)[ 'name' => $name, 'displayname' => $displayname ]; } return [ 'options' => $modoptions, 'warnings' => $warnings ]; } /** * Returns for get_category_options(). * * @return external_single_structure */ public static function get_activity_options_returns() { $optionsdefinition = new external_single_structure( [ 'name' => new external_value(PARAM_TEXT, 'The plugin name of the activity'), 'displayname' => new external_value(PARAM_TEXT, 'The display name of the activity'), ] ); return new external_single_structure([ 'options' => new external_multiple_structure($optionsdefinition), 'warnings' => new external_warnings() ]); } /** * Gets the structure of a tree node (link + child branches). * * @since Moodle 3.5 * @param bool $allowchildbranches * @return array */ private static function get_tree_node_structure($allowchildbranches = true) { $fields = [ 'text' => new external_value(PARAM_RAW, 'The node text', VALUE_REQUIRED), 'expandcontextid' => new external_value(PARAM_INT, 'The contextid this node expands', VALUE_REQUIRED), 'expandelement' => new external_value(PARAM_ALPHA, 'What element is this node expanded to', VALUE_REQUIRED), 'contextid' => new external_value(PARAM_INT, 'The node contextid', VALUE_REQUIRED), 'contextlevel' => new external_value(PARAM_INT, 'The node contextlevel', VALUE_REQUIRED), 'expanded' => new external_value(PARAM_INT, 'Is it expanded', VALUE_REQUIRED), ]; if ($allowchildbranches) { // Passing false as we will not have more than 1 sub-level. $fields['branches'] = new external_multiple_structure( self::get_tree_node_structure(false), 'Children node structure', VALUE_OPTIONAL ); } else { // We will only have 1 sub-level and we don't want an infinite get_tree_node_structure, this is a hacky // way to prevent this infinite loop when calling get_tree_node_structure recursively. $fields['branches'] = new external_multiple_structure( new external_value( PARAM_TEXT, 'Nothing really, it will always be an empty array', VALUE_OPTIONAL ) ); } return new external_single_structure($fields, 'Node structure', VALUE_OPTIONAL); } } classes/purpose.php 0000644 00000013755 15152701722 0010424 0 ustar 00 <?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/>. /** * Class for loading/storing data purposes from the DB. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy; use stdClass; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php'); /** * Class for loading/storing data purposes from the DB. * * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class purpose extends \core\persistent { /** * Database table. */ const TABLE = 'tool_dataprivacy_purpose'; /** Items under GDPR Article 6.1. */ const GDPR_ART_6_1_ITEMS = ['a', 'b', 'c', 'd', 'e', 'f']; /** Items under GDPR Article 9.2. */ const GDPR_ART_9_2_ITEMS = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']; /** * Extended constructor to fetch from the cache if available. * * @param int $id If set, this is the id of an existing record, used to load the data. * @param stdClass $record If set will be passed to {@link self::from_record()}. */ public function __construct($id = 0, stdClass $record = null) { global $CFG; if ($id) { $cache = \cache::make('tool_dataprivacy', 'purpose'); if ($data = $cache->get($id)) { // Replicate self::read. $this->from_record($data); // Validate the purpose record. $this->validate(); // Now replicate the parent constructor. if (!empty($record)) { $this->from_record($record); } if ($CFG->debugdeveloper) { $this->verify_protected_methods(); } return; } } parent::__construct($id, $record); } /** * Return the definition of the properties of this model. * * @return array */ protected static function define_properties() { return array( 'name' => array( 'type' => PARAM_TEXT, 'description' => 'The purpose name.', ), 'description' => array( 'type' => PARAM_RAW, 'description' => 'The purpose description.', 'null' => NULL_ALLOWED, 'default' => '', ), 'descriptionformat' => array( 'choices' => array(FORMAT_HTML, FORMAT_MOODLE, FORMAT_PLAIN, FORMAT_MARKDOWN), 'type' => PARAM_INT, 'default' => FORMAT_HTML ), 'lawfulbases' => array( 'type' => PARAM_TEXT, 'description' => 'Comma-separated IDs matching records in tool_dataprivacy_lawfulbasis.', ), 'sensitivedatareasons' => array( 'type' => PARAM_TEXT, 'description' => 'Comma-separated IDs matching records in tool_dataprivacy_sensitive', 'null' => NULL_ALLOWED, 'default' => '' ), 'retentionperiod' => array( 'type' => PARAM_ALPHANUM, 'description' => 'Retention period. ISO_8601 durations format (as in DateInterval format).', 'default' => '', ), 'protected' => array( 'type' => PARAM_INT, 'description' => 'Data retention with higher precedent over user\'s request to be forgotten.', 'default' => '0', ), ); } /** * Adds the new record to the cache. * * @return null */ protected function after_create() { $cache = \cache::make('tool_dataprivacy', 'purpose'); $cache->set($this->get('id'), $this->to_record()); } /** * Updates the cache record. * * @param bool $result * @return null */ protected function after_update($result) { $cache = \cache::make('tool_dataprivacy', 'purpose'); $cache->set($this->get('id'), $this->to_record()); } /** * Removes unnecessary stuff from db. * * @return null */ protected function before_delete() { $cache = \cache::make('tool_dataprivacy', 'purpose'); $cache->delete($this->get('id')); } /** * Is this purpose used?. * * @return null */ public function is_used() { if (\tool_dataprivacy\contextlevel::is_purpose_used($this->get('id')) || \tool_dataprivacy\context_instance::is_purpose_used($this->get('id'))) { return true; } $pluginconfig = get_config('tool_dataprivacy'); $levels = \context_helper::get_all_levels(); foreach ($levels as $level => $classname) { list($purposevar, $unused) = \tool_dataprivacy\data_registry::var_names_from_context($classname); if (!empty($pluginconfig->{$purposevar}) && $pluginconfig->{$purposevar} == $this->get('id')) { return true; } } return false; } /** * Get a list of the role purpose overrides for this purpose. * * @return array */ public function get_purpose_overrides() : array { return purpose_override::get_overrides_for_purpose($this); } } classes/event/user_deleted_observer.php 0000644 00000004574 15152701722 0014422 0 ustar 00 <?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/>. /** * Event observers supported by this module. * * @package tool_dataprivacy * @copyright 2018 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\event; use \tool_dataprivacy\api; use \tool_dataprivacy\data_request; defined('MOODLE_INTERNAL') || die(); /** * Event observers supported by this module. * * @package tool_dataprivacy * @copyright 2018 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_deleted_observer { /** * Create user data deletion request when the user is deleted. * * @param \core\event\user_deleted $event */ public static function create_delete_data_request(\core\event\user_deleted $event) { // Automatic creation of deletion requests must be enabled. if (get_config('tool_dataprivacy', 'automaticdeletionrequests')) { $requesttypes = [api::DATAREQUEST_TYPE_DELETE]; $requeststatuses = [api::DATAREQUEST_STATUS_COMPLETE, api::DATAREQUEST_STATUS_DELETED]; $hasongoingdeleterequests = api::has_ongoing_request($event->objectid, $requesttypes[0]); $hascompleteddeleterequest = (api::get_data_requests_count($event->objectid, $requeststatuses, $requesttypes) > 0) ? true : false; if (!$hasongoingdeleterequests && !$hascompleteddeleterequest) { api::create_data_request($event->objectid, $requesttypes[0], get_string('datarequestcreatedupondelete', 'tool_dataprivacy'), data_request::DATAREQUEST_CREATION_AUTO); } } } } classes/category.php 0000644 00000005546 15152701722 0010543 0 ustar 00 <?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/>. /** * Class for loading/storing data categories from the DB. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php'); /** * Class for loading/storing data categories from the DB. * * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class category extends \core\persistent { /** * Database table. */ const TABLE = 'tool_dataprivacy_category'; /** * Return the definition of the properties of this model. * * @return array */ protected static function define_properties() { return array( 'name' => array( 'type' => PARAM_TEXT, 'description' => 'The category name.', ), 'description' => array( 'type' => PARAM_RAW, 'description' => 'The category description.', 'null' => NULL_ALLOWED, 'default' => '', ), 'descriptionformat' => array( 'choices' => array(FORMAT_HTML, FORMAT_MOODLE, FORMAT_PLAIN, FORMAT_MARKDOWN), 'type' => PARAM_INT, 'default' => FORMAT_HTML ), ); } /** * Is this category used?. * * @return null */ public function is_used() { if (\tool_dataprivacy\contextlevel::is_category_used($this->get('id')) || \tool_dataprivacy\context_instance::is_category_used($this->get('id'))) { return true; } $pluginconfig = get_config('tool_dataprivacy'); $levels = \context_helper::get_all_levels(); foreach ($levels as $level => $classname) { list($unused, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname); if (!empty($pluginconfig->{$categoryvar}) && $pluginconfig->{$categoryvar} == $this->get('id')) { return true; } } return false; } } classes/task/expired_retention_period.php 0000644 00000003620 15152701722 0014750 0 ustar 00 <?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/>. /** * Scheduled task to flag contexts as expired. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\task; use coding_exception; use core\task\scheduled_task; use tool_dataprivacy\api; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php'); /** * Scheduled task to flag contexts as expired. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class expired_retention_period extends scheduled_task { /** * Returns the task name. * * @return string */ public function get_name() { return get_string('expiredretentionperiodtask', 'tool_dataprivacy'); } /** * Run the task to flag context instances as expired. */ public function execute() { $manager = new \tool_dataprivacy\expired_contexts_manager(new \text_progress_trace()); list($courses, $users) = $manager->flag_expired_contexts(); mtrace("Flagged {$courses} course contexts, and {$users} user contexts as expired"); } } classes/task/delete_existing_deleted_users.php 0000644 00000005772 15152701722 0015754 0 ustar 00 <?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/>. /** * Scheduled task to create delete data request for pre-existing deleted users. * * @package tool_dataprivacy * @copyright 2018 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\task; use core\task\scheduled_task; use tool_dataprivacy\api; use tool_dataprivacy\data_request; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php'); /** * Scheduled task to create delete data request for pre-existing deleted users. * * @package tool_dataprivacy * @copyright 2018 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class delete_existing_deleted_users extends scheduled_task { /** * Returns the task name. * * @return string */ public function get_name() { return get_string('deleteexistingdeleteduserstask', 'tool_dataprivacy'); } /** * Run the task to delete expired data request files and update request statuses. * */ public function execute() { global $DB; // Automatic creation of deletion requests must be enabled. if (get_config('tool_dataprivacy', 'automaticdeletionrequests')) { // Select all deleted users that do not have any delete data requests created for them. $sql = "SELECT DISTINCT(u.id) FROM {user} u LEFT JOIN {tool_dataprivacy_request} r ON u.id = r.userid WHERE u.deleted = ? AND (r.id IS NULL OR r.type != ?)"; $params = [ 1, api::DATAREQUEST_TYPE_DELETE ]; $deletedusers = $DB->get_records_sql($sql, $params); $createdrequests = 0; foreach ($deletedusers as $user) { api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE, get_string('datarequestcreatedfromscheduledtask', 'tool_dataprivacy'), data_request::DATAREQUEST_CREATION_AUTO); $createdrequests++; } if ($createdrequests > 0) { mtrace($createdrequests . ' delete data request(s) created for existing deleted users'); } } } } classes/task/delete_expired_requests.php 0000644 00000004106 15152701722 0014574 0 ustar 00 <?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/>. /** * Scheduled task to delete files and update statuses of expired data requests. * * @package tool_dataprivacy * @copyright 2018 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\task; use coding_exception; use core\task\scheduled_task; use tool_dataprivacy\api; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php'); /** * Scheduled task to delete files and update request statuses once they expire. * * @package tool_dataprivacy * @copyright 2018 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class delete_expired_requests extends scheduled_task { /** * Returns the task name. * * @return string */ public function get_name() { return get_string('deleteexpireddatarequeststask', 'tool_dataprivacy'); } /** * Run the task to delete expired data request files and update request statuses. * */ public function execute() { $expiredrequests = \tool_dataprivacy\data_request::get_expired_requests(); $deletecount = count($expiredrequests); if ($deletecount > 0) { \tool_dataprivacy\data_request::expire($expiredrequests); mtrace($deletecount . ' expired completed data requests have been deleted'); } } } classes/task/process_data_request_task.php 0000644 00000031706 15152701722 0015126 0 ustar 00 <?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/>. /** * Adhoc task that processes an approved data request and prepares/deletes the user's data. * * @package tool_dataprivacy * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\task; use action_link; use coding_exception; use context_system; use core\message\message; use core\task\adhoc_task; use core_user; use moodle_exception; use moodle_url; use tool_dataprivacy\api; use tool_dataprivacy\data_request; defined('MOODLE_INTERNAL') || die(); /** * Class that processes an approved data request and prepares/deletes the user's data. * * Custom data accepted: * - requestid -> The ID of the data request to be processed. * * @package tool_dataprivacy * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class process_data_request_task extends adhoc_task { /** * Run the task to initiate the data request process. * * @throws coding_exception * @throws moodle_exception */ public function execute() { global $CFG, $PAGE, $SITE; require_once($CFG->dirroot . "/{$CFG->admin}/tool/dataprivacy/lib.php"); if (!isset($this->get_custom_data()->requestid)) { throw new coding_exception('The custom data \'requestid\' is required.'); } $requestid = $this->get_custom_data()->requestid; $requestpersistent = new data_request($requestid); $request = $requestpersistent->to_record(); // Check if this request still needs to be processed. e.g. The user might have cancelled it before this task has run. $status = $requestpersistent->get('status'); if (!api::is_active($status)) { mtrace("Request {$requestid} with status {$status} doesn't need to be processed. Skipping..."); return; } if (!\tool_dataprivacy\data_registry::defaults_set()) { // Warn if no site purpose is defined. mtrace('Warning: No purpose is defined at the system level. Deletion will delete all.'); } // Grab the manager. // We set an observer against it to handle failures. $manager = new \core_privacy\manager(); $manager->set_observer(new \tool_dataprivacy\manager_observer()); // Get the user details now. We might not be able to retrieve it later if it's a deletion processing. $foruser = core_user::get_user($request->userid); // Update the status of this request as pre-processing. mtrace('Pre-processing request...'); api::update_request_status($requestid, api::DATAREQUEST_STATUS_PROCESSING); $contextlistcollection = $manager->get_contexts_for_userid($requestpersistent->get('userid')); mtrace('Fetching approved contextlists from collection'); $approvedclcollection = api::get_approved_contextlist_collection_for_collection( $contextlistcollection, $foruser, $request->type); mtrace('Processing request...'); $completestatus = api::DATAREQUEST_STATUS_COMPLETE; $deleteuser = false; if ($request->type == api::DATAREQUEST_TYPE_EXPORT) { // Get the user context. $usercontext = \context_user::instance($foruser->id, IGNORE_MISSING); if (!$usercontext) { mtrace("Request {$requestid} cannot be processed due to a missing user context instance for the user with ID {$foruser->id}. Skipping..."); return; } // Export the data. $exportedcontent = $manager->export_user_data($approvedclcollection); $fs = get_file_storage(); $filerecord = new \stdClass; $filerecord->component = 'tool_dataprivacy'; $filerecord->contextid = $usercontext->id; $filerecord->userid = $foruser->id; $filerecord->filearea = 'export'; $filerecord->filename = 'export.zip'; $filerecord->filepath = '/'; $filerecord->itemid = $requestid; $filerecord->license = $CFG->sitedefaultlicense; $filerecord->author = fullname($foruser); // Save somewhere. $thing = $fs->create_file_from_pathname($filerecord, $exportedcontent); $completestatus = api::DATAREQUEST_STATUS_DOWNLOAD_READY; } else if ($request->type == api::DATAREQUEST_TYPE_DELETE) { // Delete the data for users other than the primary admin, which is rejected. if (is_primary_admin($foruser->id)) { $completestatus = api::DATAREQUEST_STATUS_REJECTED; } else { $manager = new \core_privacy\manager(); $manager->set_observer(new \tool_dataprivacy\manager_observer()); $manager->delete_data_for_user($approvedclcollection); $completestatus = api::DATAREQUEST_STATUS_DELETED; $deleteuser = !$foruser->deleted; } } // When the preparation of the metadata finishes, update the request status to awaiting approval. api::update_request_status($requestid, $completestatus); mtrace('The processing of the user data request has been completed...'); // Create message to notify the user regarding the processing results. $message = new message(); $message->courseid = $SITE->id; $message->component = 'tool_dataprivacy'; $message->name = 'datarequestprocessingresults'; if (empty($request->dpo)) { // Use the no-reply user as the sender if the privacy officer is not set. This is the case for automatically // approved requests. $fromuser = core_user::get_noreply_user(); } else { $fromuser = core_user::get_user($request->dpo); $message->replyto = $fromuser->email; $message->replytoname = fullname($fromuser); } $message->userfrom = $fromuser; $typetext = null; // Prepare the context data for the email message body. $messagetextdata = [ 'username' => fullname($foruser) ]; $output = $PAGE->get_renderer('tool_dataprivacy'); $emailonly = false; $notifyuser = true; switch ($request->type) { case api::DATAREQUEST_TYPE_EXPORT: // Check if the user is allowed to download their own export. (This is for // institutions which centrally co-ordinate subject access request across many // systems, not just one Moodle instance, so we don't want every instance emailing // the user.) if (!api::can_download_data_request_for_user($request->userid, $request->requestedby, $request->userid)) { $notifyuser = false; } $typetext = get_string('requesttypeexport', 'tool_dataprivacy'); // We want to notify the user in Moodle about the processing results. $message->notification = 1; $datarequestsurl = new moodle_url('/admin/tool/dataprivacy/mydatarequests.php'); $message->contexturl = $datarequestsurl; $message->contexturlname = get_string('datarequests', 'tool_dataprivacy'); // Message to the recipient. $messagetextdata['message'] = get_string('resultdownloadready', 'tool_dataprivacy', format_string($SITE->fullname, true, ['context' => context_system::instance()])); // Prepare download link. $downloadurl = moodle_url::make_pluginfile_url($usercontext->id, 'tool_dataprivacy', 'export', $thing->get_itemid(), $thing->get_filepath(), $thing->get_filename(), true); $downloadlink = new action_link($downloadurl, get_string('download', 'tool_dataprivacy')); $messagetextdata['downloadlink'] = $downloadlink->export_for_template($output); break; case api::DATAREQUEST_TYPE_DELETE: $typetext = get_string('requesttypedelete', 'tool_dataprivacy'); // No point notifying a deleted user in Moodle. $message->notification = 0; // Message to the recipient. $messagetextdata['message'] = get_string('resultdeleted', 'tool_dataprivacy', format_string($SITE->fullname, true, ['context' => context_system::instance()])); // Message will be sent to the deleted user via email only. $emailonly = true; break; default: throw new moodle_exception('errorinvalidrequesttype', 'tool_dataprivacy'); } $subject = get_string('datarequestemailsubject', 'tool_dataprivacy', $typetext); $message->subject = $subject; $message->fullmessageformat = FORMAT_HTML; $message->userto = $foruser; // Render message email body. $messagehtml = $output->render_from_template('tool_dataprivacy/data_request_results_email', $messagetextdata); $message->fullmessage = html_to_text($messagehtml); $message->fullmessagehtml = $messagehtml; // Send message to the user involved. if ($notifyuser) { $messagesent = false; if ($emailonly) { // Do not sent an email if the user has been deleted. The user email has been previously deleted. if (!$foruser->deleted) { $messagesent = email_to_user($foruser, $fromuser, $subject, $message->fullmessage, $messagehtml); } } else { $messagesent = message_send($message); } if ($messagesent) { mtrace('Message sent to user: ' . $messagetextdata['username']); } } // Send to requester as well in some circumstances. if ($foruser->id != $request->requestedby) { $sendtorequester = false; switch ($request->type) { case api::DATAREQUEST_TYPE_EXPORT: // Send to the requester as well if they can download it, unless they are the // DPO. If we didn't notify the user themselves (because they can't download) // then send to requester even if it is the DPO, as in that case the requester // needs to take some action. if (api::can_download_data_request_for_user($request->userid, $request->requestedby, $request->requestedby)) { $sendtorequester = !$notifyuser || !api::is_site_dpo($request->requestedby); } break; case api::DATAREQUEST_TYPE_DELETE: // Send to the requester if they are not the DPO and if they are allowed to // create data requests for the user (e.g. Parent). $sendtorequester = !api::is_site_dpo($request->requestedby) && api::can_create_data_request_for_user($request->userid, $request->requestedby); break; default: throw new moodle_exception('errorinvalidrequesttype', 'tool_dataprivacy'); } // Ensure the requester has the capability to make data requests for this user. if ($sendtorequester) { $requestedby = core_user::get_user($request->requestedby); $message->userto = $requestedby; $messagetextdata['username'] = fullname($requestedby); // Render message email body. $messagehtml = $output->render_from_template('tool_dataprivacy/data_request_results_email', $messagetextdata); $message->fullmessage = html_to_text($messagehtml); $message->fullmessagehtml = $messagehtml; // Send message. if ($emailonly) { email_to_user($requestedby, $fromuser, $subject, $message->fullmessage, $messagehtml); } else { message_send($message); } mtrace('Message sent to requester: ' . $messagetextdata['username']); } } if ($deleteuser) { // Delete the user. delete_user($foruser); } } } classes/task/delete_expired_contexts.php 0000644 00000004023 15152701722 0014566 0 ustar 00 <?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/>. /** * Scheduled task to delete expired context instances once they are approved for deletion. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\task; use coding_exception; use core\task\scheduled_task; use tool_dataprivacy\api; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php'); /** * Scheduled task to delete expired context instances once they are approved for deletion. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class delete_expired_contexts extends scheduled_task { /** * Returns the task name. * * @return string */ public function get_name() { return get_string('deleteexpiredcontextstask', 'tool_dataprivacy'); } /** * Run the task to delete context instances based on their retention periods. */ public function execute() { $manager = new \tool_dataprivacy\expired_contexts_manager(new \text_progress_trace()); list($courses, $users) = $manager->process_approved_deletions(); mtrace("Processed deletions for {$courses} course contexts, and {$users} user contexts as expired"); } } classes/data_registry.php 0000644 00000034147 15152701722 0011566 0 ustar 00 <?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/>. /** * Data registry business logic methods. Mostly internal stuff. * * All methods should be considered part of the internal tool_dataprivacy API * unless something different is specified. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy; use coding_exception; use core\persistent; defined('MOODLE_INTERNAL') || die(); /** * Data registry business logic methods. Mostly internal stuff. * * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class data_registry { /** * Returns purpose and category var names from a context class name * * @param string $classname The context level's class. * @param string $pluginname The name of the plugin associated with the context level. * @return string[] */ public static function var_names_from_context($classname, $pluginname = '') { $pluginname = trim($pluginname ?? ''); if (!empty($pluginname)) { $categoryvar = $classname . '_' . $pluginname . '_category'; $purposevar = $classname . '_' . $pluginname . '_purpose'; } else { $categoryvar = $classname . '_category'; $purposevar = $classname . '_purpose'; } return [ $purposevar, $categoryvar ]; } /** * Returns the default purpose id and category id for the provided context level. * * The caller code is responsible of checking that $contextlevel is an integer. * * @param int $contextlevel The context level. * @param string $pluginname The name of the plugin associated with the context level. * @return int[]|false[] */ public static function get_defaults($contextlevel, $pluginname = '') { $classname = \context_helper::get_class_for_level($contextlevel); list($purposevar, $categoryvar) = self::var_names_from_context($classname, $pluginname); $purposeid = get_config('tool_dataprivacy', $purposevar); $categoryid = get_config('tool_dataprivacy', $categoryvar); if (!empty($pluginname)) { list($purposevar, $categoryvar) = self::var_names_from_context($classname); // If the plugin-level doesn't have a default purpose set, try the context level. if ($purposeid == false) { $purposeid = get_config('tool_dataprivacy', $purposevar); } // If the plugin-level doesn't have a default category set, try the context level. if ($categoryid == false) { $categoryid = get_config('tool_dataprivacy', $categoryvar); } } if (empty($purposeid)) { $purposeid = context_instance::NOTSET; } if (empty($categoryid)) { $categoryid = context_instance::NOTSET; } return [$purposeid, $categoryid]; } /** * Are data registry defaults set? * * At least the system defaults need to be set. * * @return bool */ public static function defaults_set() { list($purposeid, $categoryid) = self::get_defaults(CONTEXT_SYSTEM); if (empty($purposeid) || empty($categoryid)) { return false; } return true; } /** * Returns all site categories that are visible to the current user. * * @return \core_course_category[] */ public static function get_site_categories() { global $DB; if (method_exists('\core_course_category', 'get_all')) { $categories = \core_course_category::get_all(['returnhidden' => true]); } else { // Fallback (to be removed once this gets integrated into master). $ids = $DB->get_fieldset_select('course_categories', 'id', ''); $categories = \core_course_category::get_many($ids); } foreach ($categories as $key => $category) { if (!$category->is_uservisible()) { unset($categories[$key]); } } return $categories; } /** * Returns the roles assigned to the provided level. * * Important to note that it returns course-level assigned roles * if the provided context level is below course. * * @param \context $context * @return array */ public static function get_subject_scope(\context $context) { if ($contextcourse = $context->get_course_context(false)) { // Below course level we look at module or block level roles + course-assigned roles. $courseroles = get_roles_used_in_context($contextcourse, false); $roles = $courseroles + get_roles_used_in_context($context, false); } else { // We list category + system for others (we don't work with user instances so no need to work about them). $roles = get_roles_used_in_context($context); } return array_map(function($role) { if ($role->name) { return $role->name; } else { return $role->shortname; } }, $roles); } /** * Returns the effective value given a context instance * * @param \context $context * @param string $element 'category' or 'purpose' * @param int|false $forcedvalue Use this value as if this was this context instance value. * @return persistent|false It return a 'purpose' instance or a 'category' instance, depending on $element */ public static function get_effective_context_value(\context $context, $element, $forcedvalue = false) { global $DB; if ($element !== 'purpose' && $element !== 'category') { throw new coding_exception('Only \'purpose\' and \'category\' are supported.'); } $fieldname = $element . 'id'; if (!empty($forcedvalue) && ($forcedvalue == context_instance::INHERIT)) { // Do not include the current context when calculating the value. // This has the effect that an inheritted value is calculated. $parentcontextids = $context->get_parent_context_ids(false); } else if (!empty($forcedvalue) && ($forcedvalue != context_instance::NOTSET)) { return self::get_element_instance($element, $forcedvalue); } else { // Fetch all parent contexts, including self. $parentcontextids = $context->get_parent_context_ids(true); } list($insql, $inparams) = $DB->get_in_or_equal($parentcontextids, SQL_PARAMS_NAMED); $inparams['contextmodule'] = CONTEXT_MODULE; if ('purpose' === $element) { $elementjoin = 'LEFT JOIN {tool_dataprivacy_purpose} ele ON ctxins.purposeid = ele.id'; $elementfields = purpose::get_sql_fields('ele', 'ele'); } else { $elementjoin = 'LEFT JOIN {tool_dataprivacy_category} ele ON ctxins.categoryid = ele.id'; $elementfields = category::get_sql_fields('ele', 'ele'); } $contextfields = \context_helper::get_preload_record_columns_sql('ctx'); $fields = implode(', ', ['ctx.id', 'm.name AS modname', $contextfields, $elementfields]); $sql = "SELECT $fields FROM {context} ctx LEFT JOIN {tool_dataprivacy_ctxinstance} ctxins ON ctx.id = ctxins.contextid LEFT JOIN {course_modules} cm ON ctx.contextlevel = :contextmodule AND ctx.instanceid = cm.id LEFT JOIN {modules} m ON m.id = cm.module {$elementjoin} WHERE ctx.id {$insql} ORDER BY ctx.path DESC"; $contextinstances = $DB->get_records_sql($sql, $inparams); // Check whether this context is a user context, or a child of a user context. // All children of a User context share the same context and cannot be set individually. foreach ($contextinstances as $record) { \context_helper::preload_from_record($record); $parent = \context::instance_by_id($record->id, false); if ($parent->contextlevel == CONTEXT_USER) { // Use the context level value for the user. return self::get_effective_contextlevel_value(CONTEXT_USER, $element); } } foreach ($contextinstances as $record) { $parent = \context::instance_by_id($record->id, false); $checkcontextlevel = false; if (empty($record->eleid)) { $checkcontextlevel = true; } if (!empty($forcedvalue) && context_instance::NOTSET == $forcedvalue) { $checkcontextlevel = true; } if ($checkcontextlevel) { // Check for a value at the contextlevel $forplugin = empty($record->modname) ? '' : $record->modname; list($purposeid, $categoryid) = self::get_effective_default_contextlevel_purpose_and_category( $parent->contextlevel, false, false, $forplugin); $instancevalue = $$fieldname; if (context_instance::NOTSET != $instancevalue && context_instance::INHERIT != $instancevalue) { // There is an actual value. Return it. return self::get_element_instance($element, $instancevalue); } } else { $elementclass = "\\tool_dataprivacy\\{$element}"; $instance = new $elementclass(null, $elementclass::extract_record($record, 'ele')); $instance->validate(); return $instance; } } throw new coding_exception('Something went wrong, system defaults should be set and we should already have a value.'); } /** * Returns the effective value for a context level. * * Note that this is different from the effective default context level * (see get_effective_default_contextlevel_purpose_and_category) as this is returning * the value set in the data registry, not in the defaults page. * * @param int $contextlevel * @param string $element 'category' or 'purpose' * @return \tool_dataprivacy\purpose|false */ public static function get_effective_contextlevel_value($contextlevel, $element) { if ($element !== 'purpose' && $element !== 'category') { throw new coding_exception('Only \'purpose\' and \'category\' are supported.'); } $fieldname = $element . 'id'; if ($contextlevel != CONTEXT_SYSTEM && $contextlevel != CONTEXT_USER) { throw new \coding_exception('Only context_system and context_user values can be retrieved, no other context levels ' . 'have a purpose or a category.'); } list($purposeid, $categoryid) = self::get_effective_default_contextlevel_purpose_and_category($contextlevel); // Note: The $$fieldname points to either $purposeid, or $categoryid. if (context_instance::NOTSET != $$fieldname && context_instance::INHERIT != $$fieldname) { // There is a specific value set. return self::get_element_instance($element, $$fieldname); } throw new coding_exception('Something went wrong, system defaults should be set and we should already have a value.'); } /** * Returns the effective default purpose and category for a context level. * * @param int $contextlevel * @param int|bool $forcedpurposevalue Use this value as if this was this context level purpose. * @param int|bool $forcedcategoryvalue Use this value as if this was this context level category. * @param string $component The name of the component to check. * @return int[] */ public static function get_effective_default_contextlevel_purpose_and_category($contextlevel, $forcedpurposevalue = false, $forcedcategoryvalue = false, $component = '') { // Get the defaults for this context level. list($purposeid, $categoryid) = self::get_defaults($contextlevel, $component); // Honour forced values. if ($forcedpurposevalue) { $purposeid = $forcedpurposevalue; } if ($forcedcategoryvalue) { $categoryid = $forcedcategoryvalue; } if ($contextlevel == CONTEXT_USER) { // Only user context levels inherit from a parent context level. list($parentpurposeid, $parentcategoryid) = self::get_defaults(CONTEXT_SYSTEM); if (context_instance::INHERIT == $purposeid || context_instance::NOTSET == $purposeid) { $purposeid = (int)$parentpurposeid; } if (context_instance::INHERIT == $categoryid || context_instance::NOTSET == $categoryid) { $categoryid = $parentcategoryid; } } return [$purposeid, $categoryid]; } /** * Returns an instance of the provided element. * * @throws \coding_exception * @param string $element The element name 'purpose' or 'category' * @param int $id The element id * @return \core\persistent */ private static function get_element_instance($element, $id) { if ($element !== 'purpose' && $element !== 'category') { throw new coding_exception('No other elements than purpose and category are allowed'); } $classname = '\tool_dataprivacy\\' . $element; return new $classname($id); } } classes/contextlevel.php 0000644 00000007731 15152701722 0011440 0 ustar 00 <?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/>. /** * Class for loading/storing context level data from the DB. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy; defined('MOODLE_INTERNAL') || die(); /** * Class for loading/storing context level data from the DB. * * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class contextlevel extends \core\persistent { /** * Database table. */ const TABLE = 'tool_dataprivacy_ctxlevel'; /** * Return the definition of the properties of this model. * * @return array */ protected static function define_properties() { return array( 'contextlevel' => array( 'type' => PARAM_INT, 'description' => 'The context level.', ), 'purposeid' => array( 'type' => PARAM_INT, 'description' => 'The purpose id.', ), 'categoryid' => array( 'type' => PARAM_INT, 'description' => 'The category id.', ), ); } /** * Returns an instance by contextlevel. * * @param mixed $contextlevel * @param mixed $exception * @return null */ public static function get_record_by_contextlevel($contextlevel, $exception = true) { global $DB; $cache = \cache::make('tool_dataprivacy', 'contextlevel'); if ($data = $cache->get($contextlevel)) { return new static(0, $data); } if (!$record = $DB->get_record(self::TABLE, array('contextlevel' => $contextlevel))) { if (!$exception) { return false; } else { throw new \dml_missing_record_exception(self::TABLE); } } return new static(0, $record); } /** * Is the provided purpose used by any contextlevel? * * @param int $purposeid * @return bool */ public static function is_purpose_used($purposeid) { global $DB; return $DB->record_exists(self::TABLE, array('purposeid' => $purposeid)); } /** * Is the provided category used by any contextlevel? * * @param int $categoryid * @return bool */ public static function is_category_used($categoryid) { global $DB; return $DB->record_exists(self::TABLE, array('categoryid' => $categoryid)); } /** * Adds the new record to the cache. * * @return null */ protected function after_create() { $cache = \cache::make('tool_dataprivacy', 'contextlevel'); $cache->set($this->get('contextlevel'), $this->to_record()); } /** * Updates the cache record. * * @param bool $result * @return null */ protected function after_update($result) { $cache = \cache::make('tool_dataprivacy', 'contextlevel'); $cache->set($this->get('contextlevel'), $this->to_record()); } /** * Removes unnecessary stuff from db. * * @return null */ protected function before_delete() { $cache = \cache::make('tool_dataprivacy', 'contextlevel'); $cache->delete($this->get('contextlevel')); } } classes/api.php 0000644 00000145140 15152701722 0007472 0 ustar 00 <?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/>. /** * Class containing helper methods for processing data requests. * * @package tool_dataprivacy * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy; use coding_exception; use context_helper; use context_system; use core\invalid_persistent_exception; use core\message\message; use core\task\manager; use core_privacy\local\request\approved_contextlist; use core_privacy\local\request\contextlist_collection; use core_user; use dml_exception; use moodle_exception; use moodle_url; use required_capability_exception; use stdClass; use tool_dataprivacy\external\data_request_exporter; use tool_dataprivacy\local\helper; use tool_dataprivacy\task\process_data_request_task; use tool_dataprivacy\data_request; defined('MOODLE_INTERNAL') || die(); /** * Class containing helper methods for processing data requests. * * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class api { /** Data export request type. */ const DATAREQUEST_TYPE_EXPORT = 1; /** Data deletion request type. */ const DATAREQUEST_TYPE_DELETE = 2; /** Other request type. Usually of enquiries to the DPO. */ const DATAREQUEST_TYPE_OTHERS = 3; /** Newly submitted and we haven't yet started finding out where they have data. */ const DATAREQUEST_STATUS_PENDING = 0; /** Metadata ready and awaiting review and approval by the Data Protection officer. */ const DATAREQUEST_STATUS_AWAITING_APPROVAL = 2; /** Request approved and will be processed soon. */ const DATAREQUEST_STATUS_APPROVED = 3; /** The request is now being processed. */ const DATAREQUEST_STATUS_PROCESSING = 4; /** Information/other request completed. */ const DATAREQUEST_STATUS_COMPLETE = 5; /** Data request cancelled by the user. */ const DATAREQUEST_STATUS_CANCELLED = 6; /** Data request rejected by the DPO. */ const DATAREQUEST_STATUS_REJECTED = 7; /** Data request download ready. */ const DATAREQUEST_STATUS_DOWNLOAD_READY = 8; /** Data request expired. */ const DATAREQUEST_STATUS_EXPIRED = 9; /** Data delete request completed, account is removed. */ const DATAREQUEST_STATUS_DELETED = 10; /** Approve data request. */ const DATAREQUEST_ACTION_APPROVE = 1; /** Reject data request. */ const DATAREQUEST_ACTION_REJECT = 2; /** * Determines whether the user can contact the site's Data Protection Officer via Moodle. * * @return boolean True when tool_dataprivacy|contactdataprotectionofficer is enabled. * @throws dml_exception */ public static function can_contact_dpo() { return get_config('tool_dataprivacy', 'contactdataprotectionofficer') == 1; } /** * Checks whether the current user has the capability to manage data requests. * * @param int $userid The user ID. * @return bool */ public static function can_manage_data_requests($userid) { // Privacy officers can manage data requests. return self::is_site_dpo($userid); } /** * Checks if the current user can manage the data registry at the provided id. * * @param int $contextid Fallback to system context id. * @throws \required_capability_exception * @return null */ public static function check_can_manage_data_registry($contextid = false) { if ($contextid) { $context = \context_helper::instance_by_id($contextid); } else { $context = \context_system::instance(); } require_capability('tool/dataprivacy:managedataregistry', $context); } /** * Fetches the list of configured privacy officer roles. * * Every time this function is called, it checks each role if they have the 'managedatarequests' capability and removes * any role that doesn't have the required capability anymore. * * @return int[] * @throws dml_exception */ public static function get_assigned_privacy_officer_roles() { $roleids = []; // Get roles from config. $configroleids = explode(',', str_replace(' ', '', get_config('tool_dataprivacy', 'dporoles'))); if (!empty($configroleids)) { // Fetch roles that have the capability to manage data requests. $capableroles = array_keys(get_roles_with_capability('tool/dataprivacy:managedatarequests')); // Extract the configured roles that have the capability from the list of capable roles. $roleids = array_intersect($capableroles, $configroleids); } return $roleids; } /** * Fetches the role shortnames of Data Protection Officer roles. * * @return array An array of the DPO role shortnames */ public static function get_dpo_role_names() : array { global $DB; $dporoleids = self::get_assigned_privacy_officer_roles(); $dponames = array(); if (!empty($dporoleids)) { list($insql, $inparams) = $DB->get_in_or_equal($dporoleids); $dponames = $DB->get_fieldset_select('role', 'shortname', "id {$insql}", $inparams); } return $dponames; } /** * Fetches the list of users with the Privacy Officer role. */ public static function get_site_dpos() { // Get role(s) that can manage data requests. $dporoles = self::get_assigned_privacy_officer_roles(); $dpos = []; $context = context_system::instance(); foreach ($dporoles as $roleid) { $userfieldsapi = \core_user\fields::for_name(); $allnames = $userfieldsapi->get_sql('u', false, '', '', false)->selects; $fields = 'u.id, u.confirmed, u.username, '. $allnames . ', ' . 'u.maildisplay, u.mailformat, u.maildigest, u.email, u.emailstop, u.city, '. 'u.country, u.picture, u.idnumber, u.department, u.institution, '. 'u.lang, u.timezone, u.lastaccess, u.mnethostid, u.auth, u.suspended, u.deleted, ' . 'r.name AS rolename, r.sortorder, '. 'r.shortname AS roleshortname, rn.name AS rolecoursealias'; // Fetch users that can manage data requests. $dpos += get_role_users($roleid, $context, false, $fields); } // If the site has no data protection officer, defer to site admin(s). if (empty($dpos)) { $dpos = get_admins(); } return $dpos; } /** * Checks whether a given user is a site Privacy Officer. * * @param int $userid The user ID. * @return bool */ public static function is_site_dpo($userid) { $dpos = self::get_site_dpos(); return array_key_exists($userid, $dpos) || is_siteadmin(); } /** * Lodges a data request and sends the request details to the site Data Protection Officer(s). * * @param int $foruser The user whom the request is being made for. * @param int $type The request type. * @param string $comments Request comments. * @param int $creationmethod The creation method of the data request. * @param bool $notify Notify DPOs of this pending request. * @return data_request * @throws invalid_persistent_exception * @throws coding_exception */ public static function create_data_request($foruser, $type, $comments = '', $creationmethod = data_request::DATAREQUEST_CREATION_MANUAL, $notify = null ) { global $USER; if (null === $notify) { // Only if notifications have not been decided by caller. if ( data_request::DATAREQUEST_CREATION_AUTO == $creationmethod) { // If the request was automatically created, then do not notify unless explicitly set. $notify = false; } else { $notify = true; } } $datarequest = new data_request(); // The user the request is being made for. $datarequest->set('userid', $foruser); // The cron is considered to be a guest user when it creates a data request. // NOTE: This should probably be changed. We should leave the default value for $requestinguser if // the request is not explicitly created by a specific user. $requestinguser = (isguestuser() && $creationmethod == data_request::DATAREQUEST_CREATION_AUTO) ? get_admin()->id : $USER->id; // The user making the request. $datarequest->set('requestedby', $requestinguser); // Set status. $status = self::DATAREQUEST_STATUS_AWAITING_APPROVAL; if (self::is_automatic_request_approval_on($type)) { // Set status to approved if automatic data request approval is enabled. $status = self::DATAREQUEST_STATUS_APPROVED; // Set the privacy officer field if the one making the data request is a privacy officer. if (self::is_site_dpo($requestinguser)) { $datarequest->set('dpo', $requestinguser); } // Mark this request as system approved. $datarequest->set('systemapproved', true); // No need to notify privacy officer(s) about automatically approved data requests. $notify = false; } $datarequest->set('status', $status); // Set request type. $datarequest->set('type', $type); // Set request comments. $datarequest->set('comments', $comments); // Set the creation method. $datarequest->set('creationmethod', $creationmethod); // Store subject access request. $datarequest->create(); // Queue the ad-hoc task for automatically approved data requests. if ($status == self::DATAREQUEST_STATUS_APPROVED) { $userid = null; if ($type == self::DATAREQUEST_TYPE_EXPORT) { $userid = $foruser; } self::queue_data_request_task($datarequest->get('id'), $userid); } if ($notify) { // Get the list of the site Data Protection Officers. $dpos = self::get_site_dpos(); // Email the data request to the Data Protection Officer(s)/Admin(s). foreach ($dpos as $dpo) { self::notify_dpo($dpo, $datarequest); } } return $datarequest; } /** * Fetches the list of the data requests. * * If user ID is provided, it fetches the data requests for the user. * Otherwise, it fetches all of the data requests, provided that the user has the capability to manage data requests. * (e.g. Users with the Data Protection Officer roles) * * @param int $userid The User ID. * @param int[] $statuses The status filters. * @param int[] $types The request type filters. * @param int[] $creationmethods The request creation method filters. * @param string $sort The order by clause. * @param int $offset Amount of records to skip. * @param int $limit Amount of records to fetch. * @return data_request[] * @throws coding_exception * @throws dml_exception */ public static function get_data_requests($userid = 0, $statuses = [], $types = [], $creationmethods = [], $sort = '', $offset = 0, $limit = 0) { global $DB, $USER; $results = []; $sqlparams = []; $sqlconditions = []; // Set default sort. if (empty($sort)) { $sort = 'status ASC, timemodified ASC'; } // Set status filters. if (!empty($statuses)) { list($statusinsql, $sqlparams) = $DB->get_in_or_equal($statuses, SQL_PARAMS_NAMED); $sqlconditions[] = "status $statusinsql"; } // Set request type filter. if (!empty($types)) { list($typeinsql, $typeparams) = $DB->get_in_or_equal($types, SQL_PARAMS_NAMED); $sqlconditions[] = "type $typeinsql"; $sqlparams = array_merge($sqlparams, $typeparams); } // Set request creation method filter. if (!empty($creationmethods)) { list($typeinsql, $typeparams) = $DB->get_in_or_equal($creationmethods, SQL_PARAMS_NAMED); $sqlconditions[] = "creationmethod $typeinsql"; $sqlparams = array_merge($sqlparams, $typeparams); } if ($userid) { // Get the data requests for the user or data requests made by the user. $sqlconditions[] = "(userid = :userid OR requestedby = :requestedby)"; $params = [ 'userid' => $userid, 'requestedby' => $userid ]; // Build a list of user IDs that the user is allowed to make data requests for. // Of course, the user should be included in this list. $alloweduserids = [$userid]; // Get any users that the user can make data requests for. if ($children = helper::get_children_of_user($userid)) { // Get the list of user IDs of the children and merge to the allowed user IDs. $alloweduserids = array_merge($alloweduserids, array_keys($children)); } list($insql, $inparams) = $DB->get_in_or_equal($alloweduserids, SQL_PARAMS_NAMED); $sqlconditions[] .= "userid $insql"; $select = implode(' AND ', $sqlconditions); $params = array_merge($params, $inparams, $sqlparams); $results = data_request::get_records_select($select, $params, $sort, '*', $offset, $limit); } else { // If the current user is one of the site's Data Protection Officers, then fetch all data requests. if (self::is_site_dpo($USER->id)) { if (!empty($sqlconditions)) { $select = implode(' AND ', $sqlconditions); $results = data_request::get_records_select($select, $sqlparams, $sort, '*', $offset, $limit); } else { $results = data_request::get_records(null, $sort, '', $offset, $limit); } } } // If any are due to expire, expire them and re-fetch updated data. if (empty($statuses) || in_array(self::DATAREQUEST_STATUS_DOWNLOAD_READY, $statuses) || in_array(self::DATAREQUEST_STATUS_EXPIRED, $statuses)) { $expiredrequests = data_request::get_expired_requests($userid); if (!empty($expiredrequests)) { data_request::expire($expiredrequests); $results = self::get_data_requests($userid, $statuses, $types, $creationmethods, $sort, $offset, $limit); } } return $results; } /** * Fetches the count of data request records based on the given parameters. * * @param int $userid The User ID. * @param int[] $statuses The status filters. * @param int[] $types The request type filters. * @param int[] $creationmethods The request creation method filters. * @return int * @throws coding_exception * @throws dml_exception */ public static function get_data_requests_count($userid = 0, $statuses = [], $types = [], $creationmethods = []) { global $DB, $USER; $count = 0; $sqlparams = []; $sqlconditions = []; if (!empty($statuses)) { list($statusinsql, $sqlparams) = $DB->get_in_or_equal($statuses, SQL_PARAMS_NAMED); $sqlconditions[] = "status $statusinsql"; } if (!empty($types)) { list($typeinsql, $typeparams) = $DB->get_in_or_equal($types, SQL_PARAMS_NAMED); $sqlconditions[] = "type $typeinsql"; $sqlparams = array_merge($sqlparams, $typeparams); } if (!empty($creationmethods)) { list($typeinsql, $typeparams) = $DB->get_in_or_equal($creationmethods, SQL_PARAMS_NAMED); $sqlconditions[] = "creationmethod $typeinsql"; $sqlparams = array_merge($sqlparams, $typeparams); } if ($userid) { // Get the data requests for the user or data requests made by the user. $sqlconditions[] = "(userid = :userid OR requestedby = :requestedby)"; $params = [ 'userid' => $userid, 'requestedby' => $userid ]; // Build a list of user IDs that the user is allowed to make data requests for. // Of course, the user should be included in this list. $alloweduserids = [$userid]; // Get any users that the user can make data requests for. if ($children = helper::get_children_of_user($userid)) { // Get the list of user IDs of the children and merge to the allowed user IDs. $alloweduserids = array_merge($alloweduserids, array_keys($children)); } list($insql, $inparams) = $DB->get_in_or_equal($alloweduserids, SQL_PARAMS_NAMED); $sqlconditions[] .= "userid $insql"; $select = implode(' AND ', $sqlconditions); $params = array_merge($params, $inparams, $sqlparams); $count = data_request::count_records_select($select, $params); } else { // If the current user is one of the site's Data Protection Officers, then fetch all data requests. if (self::is_site_dpo($USER->id)) { if (!empty($sqlconditions)) { $select = implode(' AND ', $sqlconditions); $count = data_request::count_records_select($select, $sqlparams); } else { $count = data_request::count_records(); } } } return $count; } /** * Checks whether there is already an existing pending/in-progress data request for a user for a given request type. * * @param int $userid The user ID. * @param int $type The request type. * @return bool * @throws coding_exception * @throws dml_exception */ public static function has_ongoing_request($userid, $type) { global $DB; // Check if the user already has an incomplete data request of the same type. $nonpendingstatuses = [ self::DATAREQUEST_STATUS_COMPLETE, self::DATAREQUEST_STATUS_CANCELLED, self::DATAREQUEST_STATUS_REJECTED, self::DATAREQUEST_STATUS_DOWNLOAD_READY, self::DATAREQUEST_STATUS_EXPIRED, self::DATAREQUEST_STATUS_DELETED, ]; list($insql, $inparams) = $DB->get_in_or_equal($nonpendingstatuses, SQL_PARAMS_NAMED, 'st', false); $select = "type = :type AND userid = :userid AND status {$insql}"; $params = array_merge([ 'type' => $type, 'userid' => $userid ], $inparams); return data_request::record_exists_select($select, $params); } /** * Find whether any ongoing requests exist for a set of users. * * @param array $userids * @return array */ public static function find_ongoing_request_types_for_users(array $userids) : array { global $DB; if (empty($userids)) { return []; } // Check if the user already has an incomplete data request of the same type. $nonpendingstatuses = [ self::DATAREQUEST_STATUS_COMPLETE, self::DATAREQUEST_STATUS_CANCELLED, self::DATAREQUEST_STATUS_REJECTED, self::DATAREQUEST_STATUS_DOWNLOAD_READY, self::DATAREQUEST_STATUS_EXPIRED, self::DATAREQUEST_STATUS_DELETED, ]; list($statusinsql, $statusparams) = $DB->get_in_or_equal($nonpendingstatuses, SQL_PARAMS_NAMED, 'st', false); list($userinsql, $userparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED, 'us'); $select = "userid {$userinsql} AND status {$statusinsql}"; $params = array_merge($statusparams, $userparams); $requests = $DB->get_records_select(data_request::TABLE, $select, $params, 'userid', 'id, userid, type'); $returnval = []; foreach ($userids as $userid) { $returnval[$userid] = (object) []; } foreach ($requests as $request) { $returnval[$request->userid]->{$request->type} = true; } return $returnval; } /** * Determines whether a request is active or not based on its status. * * @param int $status The request status. * @return bool */ public static function is_active($status) { // List of statuses which doesn't require any further processing. $finalstatuses = [ self::DATAREQUEST_STATUS_COMPLETE, self::DATAREQUEST_STATUS_CANCELLED, self::DATAREQUEST_STATUS_REJECTED, self::DATAREQUEST_STATUS_DOWNLOAD_READY, self::DATAREQUEST_STATUS_EXPIRED, self::DATAREQUEST_STATUS_DELETED, ]; return !in_array($status, $finalstatuses); } /** * Cancels the data request for a given request ID. * * @param int $requestid The request identifier. * @param int $status The request status. * @param int $dpoid The user ID of the Data Protection Officer * @param string $comment The comment about the status update. * @return bool * @throws invalid_persistent_exception * @throws coding_exception */ public static function update_request_status($requestid, $status, $dpoid = 0, $comment = '') { // Update the request. $datarequest = new data_request($requestid); $datarequest->set('status', $status); if ($dpoid) { $datarequest->set('dpo', $dpoid); } // Update the comment if necessary. if (!empty(trim($comment))) { $params = [ 'date' => userdate(time()), 'comment' => $comment ]; $commenttosave = get_string('datecomment', 'tool_dataprivacy', $params); // Check if there's an existing DPO comment. $currentcomment = trim($datarequest->get('dpocomment')); if ($currentcomment) { // Append the new comment to the current comment and give them 1 line space in between. $commenttosave = $currentcomment . PHP_EOL . PHP_EOL . $commenttosave; } $datarequest->set('dpocomment', $commenttosave); } return $datarequest->update(); } /** * Fetches a request based on the request ID. * * @param int $requestid The request identifier * @return data_request */ public static function get_request($requestid) { return new data_request($requestid); } /** * Approves a data request based on the request ID. * * @param int $requestid The request identifier * @return bool * @throws coding_exception * @throws dml_exception * @throws invalid_persistent_exception * @throws required_capability_exception * @throws moodle_exception */ public static function approve_data_request($requestid) { global $USER; // Check first whether the user can manage data requests. if (!self::can_manage_data_requests($USER->id)) { $context = context_system::instance(); throw new required_capability_exception($context, 'tool/dataprivacy:managedatarequests', 'nopermissions', ''); } // Check if request is already awaiting for approval. $request = new data_request($requestid); if ($request->get('status') != self::DATAREQUEST_STATUS_AWAITING_APPROVAL) { throw new moodle_exception('errorrequestnotwaitingforapproval', 'tool_dataprivacy'); } // Check if current user has permission to approve delete data request. if ($request->get('type') == self::DATAREQUEST_TYPE_DELETE && !self::can_create_data_deletion_request_for_other()) { throw new required_capability_exception(context_system::instance(), 'tool/dataprivacy:requestdeleteforotheruser', 'nopermissions', ''); } // Update the status and the DPO. $result = self::update_request_status($requestid, self::DATAREQUEST_STATUS_APPROVED, $USER->id); // Fire an ad hoc task to initiate the data request process. $userid = null; if ($request->get('type') == self::DATAREQUEST_TYPE_EXPORT) { $userid = $request->get('userid'); } self::queue_data_request_task($requestid, $userid); return $result; } /** * Rejects a data request based on the request ID. * * @param int $requestid The request identifier * @return bool * @throws coding_exception * @throws dml_exception * @throws invalid_persistent_exception * @throws required_capability_exception * @throws moodle_exception */ public static function deny_data_request($requestid) { global $USER; if (!self::can_manage_data_requests($USER->id)) { $context = context_system::instance(); throw new required_capability_exception($context, 'tool/dataprivacy:managedatarequests', 'nopermissions', ''); } // Check if request is already awaiting for approval. $request = new data_request($requestid); if ($request->get('status') != self::DATAREQUEST_STATUS_AWAITING_APPROVAL) { throw new moodle_exception('errorrequestnotwaitingforapproval', 'tool_dataprivacy'); } // Check if current user has permission to reject delete data request. if ($request->get('type') == self::DATAREQUEST_TYPE_DELETE && !self::can_create_data_deletion_request_for_other()) { throw new required_capability_exception(context_system::instance(), 'tool/dataprivacy:requestdeleteforotheruser', 'nopermissions', ''); } // Update the status and the DPO. return self::update_request_status($requestid, self::DATAREQUEST_STATUS_REJECTED, $USER->id); } /** * Sends a message to the site's Data Protection Officer about a request. * * @param stdClass $dpo The DPO user record * @param data_request $request The data request * @return int|false * @throws coding_exception * @throws moodle_exception */ public static function notify_dpo($dpo, data_request $request) { global $PAGE, $SITE; $output = $PAGE->get_renderer('tool_dataprivacy'); $usercontext = \context_user::instance($request->get('requestedby')); $requestexporter = new data_request_exporter($request, ['context' => $usercontext]); $requestdata = $requestexporter->export($output); // Create message to send to the Data Protection Officer(s). $typetext = null; $typetext = $requestdata->typename; $subject = get_string('datarequestemailsubject', 'tool_dataprivacy', $typetext); $requestedby = $requestdata->requestedbyuser; $datarequestsurl = new moodle_url('/admin/tool/dataprivacy/datarequests.php'); $message = new message(); $message->courseid = $SITE->id; $message->component = 'tool_dataprivacy'; $message->name = 'contactdataprotectionofficer'; $message->userfrom = $requestedby->id; $message->replyto = $requestedby->email; $message->replytoname = $requestedby->fullname; $message->subject = $subject; $message->fullmessageformat = FORMAT_HTML; $message->notification = 1; $message->contexturl = $datarequestsurl; $message->contexturlname = get_string('datarequests', 'tool_dataprivacy'); // Prepare the context data for the email message body. $messagetextdata = [ 'requestedby' => $requestedby->fullname, 'requesttype' => $typetext, 'requestdate' => userdate($requestdata->timecreated), 'requestorigin' => format_string($SITE->fullname, true, ['context' => context_system::instance()]), 'requestoriginurl' => new moodle_url('/'), 'requestcomments' => $requestdata->messagehtml, 'datarequestsurl' => $datarequestsurl ]; $requestingfor = $requestdata->foruser; if ($requestedby->id == $requestingfor->id) { $messagetextdata['requestfor'] = $messagetextdata['requestedby']; } else { $messagetextdata['requestfor'] = $requestingfor->fullname; } // Email the data request to the Data Protection Officer(s)/Admin(s). $messagetextdata['dponame'] = fullname($dpo); // Render message email body. $messagehtml = $output->render_from_template('tool_dataprivacy/data_request_email', $messagetextdata); $message->userto = $dpo; $message->fullmessage = html_to_text($messagehtml); $message->fullmessagehtml = $messagehtml; // Send message. return message_send($message); } /** * Checks whether a non-DPO user can make a data request for another user. * * @param int $user The user ID of the target user. * @param int $requester The user ID of the user making the request. * @return bool */ public static function can_create_data_request_for_user($user, $requester = null) { $usercontext = \context_user::instance($user); return has_capability('tool/dataprivacy:makedatarequestsforchildren', $usercontext, $requester); } /** * Require that the current user can make a data request for the specified other user. * * @param int $user The user ID of the target user. * @param int $requester The user ID of the user making the request. * @return bool */ public static function require_can_create_data_request_for_user($user, $requester = null) { $usercontext = \context_user::instance($user); require_capability('tool/dataprivacy:makedatarequestsforchildren', $usercontext, $requester); return true; } /** * Check if user has permission to create data download request for themselves * * @param int|null $userid * @return bool */ public static function can_create_data_download_request_for_self(int $userid = null): bool { global $USER; $userid = $userid ?: $USER->id; return has_capability('tool/dataprivacy:downloadownrequest', \context_user::instance($userid), $userid); } /** * Check if user has permisson to create data deletion request for themselves. * * @param int|null $userid ID of the user. * @return bool * @throws coding_exception */ public static function can_create_data_deletion_request_for_self(int $userid = null): bool { global $USER; $userid = $userid ?: $USER->id; return has_capability('tool/dataprivacy:requestdelete', \context_user::instance($userid), $userid) && !is_primary_admin($userid); } /** * Check if user has permission to create data deletion request for another user. * * @param int|null $userid ID of the user. * @return bool * @throws coding_exception * @throws dml_exception */ public static function can_create_data_deletion_request_for_other(int $userid = null): bool { global $USER; $userid = $userid ?: $USER->id; return has_capability('tool/dataprivacy:requestdeleteforotheruser', context_system::instance(), $userid); } /** * Check if parent can create data deletion request for their children. * * @param int $userid ID of a user being requested. * @param int|null $requesterid ID of a user making request. * @return bool * @throws coding_exception */ public static function can_create_data_deletion_request_for_children(int $userid, int $requesterid = null): bool { global $USER; $requesterid = $requesterid ?: $USER->id; return has_capability('tool/dataprivacy:makedatadeletionrequestsforchildren', \context_user::instance($userid), $requesterid) && !is_primary_admin($userid); } /** * Checks whether a user can download a data request. * * @param int $userid Target user id (subject of data request) * @param int $requesterid Requester user id (person who requsted it) * @param int|null $downloaderid Person who wants to download user id (default current) * @return bool * @throws coding_exception */ public static function can_download_data_request_for_user($userid, $requesterid, $downloaderid = null) { global $USER; if (!$downloaderid) { $downloaderid = $USER->id; } $usercontext = \context_user::instance($userid); // If it's your own and you have the right capability, you can download it. if ($userid == $downloaderid && self::can_create_data_download_request_for_self($downloaderid)) { return true; } // If you can download anyone's in that context, you can download it. if (has_capability('tool/dataprivacy:downloadallrequests', $usercontext, $downloaderid)) { return true; } // If you can have the 'child access' ability to request in that context, and you are the one // who requested it, then you can download it. if ($requesterid == $downloaderid && self::can_create_data_request_for_user($userid, $requesterid)) { return true; } return false; } /** * Gets an action menu link to download a data request. * * @param \context_user $usercontext User context (of user who the data is for) * @param int $requestid Request id * @return \action_menu_link_secondary Action menu link * @throws coding_exception */ public static function get_download_link(\context_user $usercontext, $requestid) { $downloadurl = moodle_url::make_pluginfile_url($usercontext->id, 'tool_dataprivacy', 'export', $requestid, '/', 'export.zip', true); $downloadtext = get_string('download', 'tool_dataprivacy'); return new \action_menu_link_secondary($downloadurl, null, $downloadtext); } /** * Creates a new data purpose. * * @param stdClass $record * @return \tool_dataprivacy\purpose. */ public static function create_purpose(stdClass $record) { $purpose = new purpose(0, $record); $purpose->create(); return $purpose; } /** * Updates an existing data purpose. * * @param stdClass $record * @return \tool_dataprivacy\purpose. */ public static function update_purpose(stdClass $record) { if (!isset($record->sensitivedatareasons)) { $record->sensitivedatareasons = ''; } $purpose = new purpose($record->id); $purpose->from_record($record); $result = $purpose->update(); return $purpose; } /** * Deletes a data purpose. * * @param int $id * @return bool */ public static function delete_purpose($id) { $purpose = new purpose($id); if ($purpose->is_used()) { throw new \moodle_exception('Purpose with id ' . $id . ' can not be deleted because it is used.'); } return $purpose->delete(); } /** * Get all system data purposes. * * @return \tool_dataprivacy\purpose[] */ public static function get_purposes() { return purpose::get_records([], 'name', 'ASC'); } /** * Creates a new data category. * * @param stdClass $record * @return \tool_dataprivacy\category. */ public static function create_category(stdClass $record) { $category = new category(0, $record); $category->create(); return $category; } /** * Updates an existing data category. * * @param stdClass $record * @return \tool_dataprivacy\category. */ public static function update_category(stdClass $record) { $category = new category($record->id); $category->from_record($record); $result = $category->update(); return $category; } /** * Deletes a data category. * * @param int $id * @return bool */ public static function delete_category($id) { $category = new category($id); if ($category->is_used()) { throw new \moodle_exception('Category with id ' . $id . ' can not be deleted because it is used.'); } return $category->delete(); } /** * Get all system data categories. * * @return \tool_dataprivacy\category[] */ public static function get_categories() { return category::get_records([], 'name', 'ASC'); } /** * Sets the context instance purpose and category. * * @param \stdClass $record * @return \tool_dataprivacy\context_instance */ public static function set_context_instance($record) { if ($instance = context_instance::get_record_by_contextid($record->contextid, false)) { // Update. $instance->from_record($record); if (empty($record->purposeid) && empty($record->categoryid)) { // We accept one of them to be null but we delete it if both are null. self::unset_context_instance($instance); return; } } else { // Add. $instance = new context_instance(0, $record); } $instance->save(); return $instance; } /** * Unsets the context instance record. * * @param \tool_dataprivacy\context_instance $instance * @return null */ public static function unset_context_instance(context_instance $instance) { $instance->delete(); } /** * Sets the context level purpose and category. * * @throws \coding_exception * @param \stdClass $record * @return contextlevel */ public static function set_contextlevel($record) { global $DB; if ($record->contextlevel != CONTEXT_SYSTEM && $record->contextlevel != CONTEXT_USER) { throw new \coding_exception('Only context system and context user can set a contextlevel ' . 'purpose and retention'); } if ($contextlevel = contextlevel::get_record_by_contextlevel($record->contextlevel, false)) { // Update. $contextlevel->from_record($record); } else { // Add. $contextlevel = new contextlevel(0, $record); } $contextlevel->save(); // We sync with their defaults as we removed these options from the defaults page. $classname = \context_helper::get_class_for_level($record->contextlevel); list($purposevar, $categoryvar) = data_registry::var_names_from_context($classname); set_config($purposevar, $record->purposeid, 'tool_dataprivacy'); set_config($categoryvar, $record->categoryid, 'tool_dataprivacy'); return $contextlevel; } /** * Returns the effective category given a context instance. * * @param \context $context * @param int $forcedvalue Use this categoryid value as if this was this context instance category. * @return category|false */ public static function get_effective_context_category(\context $context, $forcedvalue = false) { if (!data_registry::defaults_set()) { return false; } return data_registry::get_effective_context_value($context, 'category', $forcedvalue); } /** * Returns the effective purpose given a context instance. * * @param \context $context * @param int $forcedvalue Use this purposeid value as if this was this context instance purpose. * @return purpose|false */ public static function get_effective_context_purpose(\context $context, $forcedvalue = false) { if (!data_registry::defaults_set()) { return false; } return data_registry::get_effective_context_value($context, 'purpose', $forcedvalue); } /** * Returns the effective category given a context level. * * @param int $contextlevel * @return category|false */ public static function get_effective_contextlevel_category($contextlevel) { if (!data_registry::defaults_set()) { return false; } return data_registry::get_effective_contextlevel_value($contextlevel, 'category'); } /** * Returns the effective purpose given a context level. * * @param int $contextlevel * @param int $forcedvalue Use this purposeid value as if this was this context level purpose. * @return purpose|false */ public static function get_effective_contextlevel_purpose($contextlevel, $forcedvalue=false) { if (!data_registry::defaults_set()) { return false; } return data_registry::get_effective_contextlevel_value($contextlevel, 'purpose', $forcedvalue); } /** * Creates an expired context record for the provided context id. * * @param int $contextid * @return \tool_dataprivacy\expired_context */ public static function create_expired_context($contextid) { $record = (object)[ 'contextid' => $contextid, 'status' => expired_context::STATUS_EXPIRED, ]; $expiredctx = new expired_context(0, $record); $expiredctx->save(); return $expiredctx; } /** * Deletes an expired context record. * * @param int $id The tool_dataprivacy_ctxexpire id. * @return bool True on success. */ public static function delete_expired_context($id) { $expiredcontext = new expired_context($id); return $expiredcontext->delete(); } /** * Updates the status of an expired context. * * @param \tool_dataprivacy\expired_context $expiredctx * @param int $status * @return null */ public static function set_expired_context_status(expired_context $expiredctx, $status) { $expiredctx->set('status', $status); $expiredctx->save(); } /** * Finds all contextlists having at least one approved context, and returns them as in a contextlist_collection. * * @param contextlist_collection $collection The collection of unapproved contextlist objects. * @param \stdClass $foruser The target user * @param int $type The purpose of the collection * @return contextlist_collection The collection of approved_contextlist objects. */ public static function get_approved_contextlist_collection_for_collection(contextlist_collection $collection, \stdClass $foruser, int $type) : contextlist_collection { // Create the approved contextlist collection object. $approvedcollection = new contextlist_collection($collection->get_userid()); $isconfigured = data_registry::defaults_set(); foreach ($collection as $contextlist) { $contextids = []; foreach ($contextlist as $context) { if ($isconfigured && self::DATAREQUEST_TYPE_DELETE == $type) { // Data can only be deleted from it if the context is either expired, or unprotected. // Note: We can only check whether a context is expired or unprotected if the site is configured and // defaults are set appropriately. If they are not, we treat all contexts as though they are // unprotected. $purpose = static::get_effective_context_purpose($context); if (!expired_contexts_manager::is_context_expired_or_unprotected_for_user($context, $foruser)) { continue; } } $contextids[] = $context->id; } // The data for the last component contextlist won't have been written yet, so write it now. if (!empty($contextids)) { $approvedcollection->add_contextlist( new approved_contextlist($foruser, $contextlist->get_component(), $contextids) ); } } return $approvedcollection; } /** * Updates the default category and purpose for a given context level (and optionally, a plugin). * * @param int $contextlevel The context level. * @param int $categoryid The ID matching the category. * @param int $purposeid The ID matching the purpose record. * @param int $activity The name of the activity that we're making a defaults configuration for. * @param bool $override Whether to override the purpose/categories of existing instances to these defaults. * @return boolean True if set/unset config succeeds. Otherwise, it throws an exception. */ public static function set_context_defaults($contextlevel, $categoryid, $purposeid, $activity = null, $override = false) { global $DB; // Get the class name associated with this context level. $classname = context_helper::get_class_for_level($contextlevel); list($purposevar, $categoryvar) = data_registry::var_names_from_context($classname, $activity); // Check the default category to be set. if ($categoryid == context_instance::INHERIT) { unset_config($categoryvar, 'tool_dataprivacy'); } else { // Make sure the given category ID exists first. $categorypersistent = new category($categoryid); $categorypersistent->read(); // Then set the new default value. set_config($categoryvar, $categoryid, 'tool_dataprivacy'); } // Check the default purpose to be set. if ($purposeid == context_instance::INHERIT) { // If the defaults is set to inherit, just unset the config value. unset_config($purposevar, 'tool_dataprivacy'); } else { // Make sure the given purpose ID exists first. $purposepersistent = new purpose($purposeid); $purposepersistent->read(); // Then set the new default value. set_config($purposevar, $purposeid, 'tool_dataprivacy'); } // Unset instances that have been assigned with custom purpose and category, if override was specified. if ($override) { // We'd like to find context IDs that we want to unset. $statements = ["SELECT c.id as contextid FROM {context} c"]; // Based on this context level. $params = ['contextlevel' => $contextlevel]; if ($contextlevel == CONTEXT_MODULE) { // If we're deleting module context instances, we need to make sure the instance ID is in the course modules table. $statements[] = "JOIN {course_modules} cm ON cm.id = c.instanceid"; // And that the module is listed on the modules table. $statements[] = "JOIN {modules} m ON m.id = cm.module"; if ($activity) { // If we're overriding for an activity module, make sure that the context instance matches that activity. $statements[] = "AND m.name = :modname"; $params['modname'] = $activity; } } // Make sure this context instance exists in the tool_dataprivacy_ctxinstance table. $statements[] = "JOIN {tool_dataprivacy_ctxinstance} tdc ON tdc.contextid = c.id"; // And that the context level of this instance matches the given context level. $statements[] = "WHERE c.contextlevel = :contextlevel"; // Build our SQL query by gluing the statements. $sql = implode("\n", $statements); // Get the context records matching our query. $contextids = $DB->get_fieldset_sql($sql, $params); // Delete the matching context instances. foreach ($contextids as $contextid) { if ($instance = context_instance::get_record_by_contextid($contextid, false)) { self::unset_context_instance($instance); } } } return true; } /** * Format the supplied date interval as a retention period. * * @param \DateInterval $interval * @return string */ public static function format_retention_period(\DateInterval $interval) : string { // It is one or another. if ($interval->y) { $formattedtime = get_string('numyears', 'moodle', $interval->format('%y')); } else if ($interval->m) { $formattedtime = get_string('nummonths', 'moodle', $interval->format('%m')); } else if ($interval->d) { $formattedtime = get_string('numdays', 'moodle', $interval->format('%d')); } else { $formattedtime = get_string('retentionperiodzero', 'tool_dataprivacy'); } return $formattedtime; } /** * Whether automatic data request approval is turned on or not for the given request type. * * @param int $type The request type. * @return bool */ public static function is_automatic_request_approval_on(int $type): bool { switch ($type) { case self::DATAREQUEST_TYPE_EXPORT: return !empty(get_config('tool_dataprivacy', 'automaticdataexportapproval')); case self::DATAREQUEST_TYPE_DELETE: return !empty(get_config('tool_dataprivacy', 'automaticdatadeletionapproval')); } return false; } /** * Creates an ad-hoc task for the data request. * * @param int $requestid The data request ID. * @param int $userid Optional. The user ID to run the task as, if necessary. */ public static function queue_data_request_task(int $requestid, int $userid = null): void { $task = new process_data_request_task(); $task->set_custom_data(['requestid' => $requestid]); if ($userid) { $task->set_userid($userid); } manager::queue_adhoc_task($task, true); } } classes/manager_observer.php 0000644 00000005611 15152701722 0012240 0 ustar 00 <?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/>. /** * Class \tool_dataprivacy\manager_observer. * * @package tool_dataprivacy * @copyright 2018 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy; defined('MOODLE_INTERNAL') || die(); /** * A failure observer for the \core_privacy\manager. * * @package tool_dataprivacy * @copyright 2018 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class manager_observer implements \core_privacy\manager_observer { /** * Notifies all DPOs that an exception occurred. * * @param \Throwable $e * @param string $component * @param string $interface * @param string $methodname * @param array $params */ public function handle_component_failure($e, $component, $interface, $methodname, array $params) { // Get the list of the site Data Protection Officers. $dpos = api::get_site_dpos(); $messagesubject = get_string('exceptionnotificationsubject', 'tool_dataprivacy'); $a = (object)[ 'fullmethodname' => \core_privacy\manager::get_provider_classname_for_component($component) . '::' . $methodname, 'component' => $component, 'message' => $e->getMessage(), 'backtrace' => $e->getTraceAsString() ]; $messagebody = get_string('exceptionnotificationbody', 'tool_dataprivacy', $a); // Email the data request to the Data Protection Officer(s)/Admin(s). foreach ($dpos as $dpo) { $message = new \core\message\message(); $message->courseid = SITEID; $message->component = 'tool_dataprivacy'; $message->name = 'notifyexceptions'; $message->userfrom = \core_user::get_noreply_user(); $message->subject = $messagesubject; $message->fullmessageformat = FORMAT_HTML; $message->notification = 1; $message->userto = $dpo; $message->fullmessagehtml = $messagebody; $message->fullmessage = html_to_text($messagebody); // Send message. message_send($message); } } } classes/metadata_registry.php 0000644 00000020306 15152701722 0012425 0 ustar 00 <?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/>. /** * Class containing helper methods for processing data requests. * * @package tool_dataprivacy * @copyright 2018 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy; defined('MOODLE_INTERNAL') || die(); /** * Class containing helper methods for processing data requests. * * @copyright 2018 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class metadata_registry { /** * Returns plugin types / plugins and the user data that it stores in a format that can be sent to a template. * * @return array An array with all of the plugin types / plugins and the user data they store. */ public function get_registry_metadata() { $manager = new \core_privacy\manager(); $manager->set_observer(new \tool_dataprivacy\manager_observer()); $pluginman = \core_plugin_manager::instance(); $contributedplugins = $this->get_contrib_list(); $metadata = $manager->get_metadata_for_components(); $fullyrichtree = $this->get_full_component_list(); foreach ($fullyrichtree as $branch => $leaves) { $plugintype = $leaves['plugin_type']; $plugins = array_map(function($component) use ($manager, $metadata, $contributedplugins, $plugintype, $pluginman) { // Use the plugin name for the plugins, ignore for core subsystems. $internaldata = ($plugintype == 'core') ? ['component' => $component] : ['component' => $pluginman->plugin_name($component)]; $internaldata['raw_component'] = $component; if ($manager->component_is_compliant($component)) { $internaldata['compliant'] = true; if (isset($metadata[$component])) { $collection = $metadata[$component]->get_collection(); $internaldata = $this->format_metadata($collection, $component, $internaldata); } else if ($manager->is_empty_subsystem($component)) { // This is an unused subsystem. // Use the generic string. $internaldata['nullprovider'] = get_string('privacy:subsystem:empty', 'core_privacy'); } else { // Call get_reason for null provider. $internaldata['nullprovider'] = get_string($manager->get_null_provider_reason($component), $component); } } else { $internaldata['compliant'] = false; } // Check to see if we are an external plugin. // Plugin names can contain _ characters, limit to 2 to just remove initial plugintype. $componentshortname = explode('_', $component, 2); $shortname = array_pop($componentshortname); if (isset($contributedplugins[$plugintype][$shortname])) { $internaldata['external'] = true; } // Additional interface checks. if (!$manager->is_empty_subsystem($component)) { $classname = $manager->get_provider_classname_for_component($component); if (class_exists($classname)) { $componentclass = new $classname(); // Check if the interface is deprecated. if ($componentclass instanceof \core_privacy\local\deprecated) { $internaldata['deprecated'] = true; } // Check that the core_userlist_provider is implemented for all user data providers. if ($componentclass instanceof \core_privacy\local\request\core_user_data_provider && !$componentclass instanceof \core_privacy\local\request\core_userlist_provider) { $internaldata['userlistnoncompliance'] = true; } // Check that any type of userlist_provider is implemented for all shared data providers. if ($componentclass instanceof \core_privacy\local\request\shared_data_provider && !$componentclass instanceof \core_privacy\local\request\userlist_provider) { $internaldata['userlistnoncompliance'] = true; } } } return $internaldata; }, $leaves['plugins']); $fullyrichtree[$branch]['plugin_type_raw'] = $plugintype; // We're done using the plugin type. Convert it to a readable string. $fullyrichtree[$branch]['plugin_type'] = $pluginman->plugintype_name($plugintype); $fullyrichtree[$branch]['plugins'] = $plugins; } return $fullyrichtree; } /** * Formats the metadata for use with a template. * * @param array $collection The collection associated with the component that we want to expand and format. * @param string $component The component that we are dealing in * @param array $internaldata The array to add the formatted metadata to. * @return array The internal data array with the formatted metadata. */ protected function format_metadata($collection, $component, $internaldata) { foreach ($collection as $collectioninfo) { $privacyfields = $collectioninfo->get_privacy_fields(); $fields = ''; if (!empty($privacyfields)) { $fields = array_map(function($key, $field) use ($component) { return [ 'field_name' => $key, 'field_summary' => get_string($field, $component) ]; }, array_keys($privacyfields), $privacyfields); } // Can the metadata types be located somewhere else besides core? $items = explode('\\', get_class($collectioninfo)); $type = array_pop($items); $typedata = [ 'name' => $collectioninfo->get_name(), 'type' => $type, 'fields' => $fields, 'summary' => get_string($collectioninfo->get_summary(), $component) ]; if (strpos($type, 'subsystem_link') === 0 || strpos($type, 'plugintype_link') === 0) { $typedata['link'] = true; } $internaldata['metadata'][] = $typedata; } return $internaldata; } /** * Return the full list of components. * * @return array An array of plugin types which contain plugin data. */ protected function get_full_component_list() { global $CFG; $list = \core_component::get_component_list(); $list['core']['core'] = "{$CFG->dirroot}/lib"; $formattedlist = []; foreach ($list as $plugintype => $plugin) { $formattedlist[] = ['plugin_type' => $plugintype, 'plugins' => array_keys($plugin)]; } return $formattedlist; } /** * Returns a list of contributed plugins installed on the system. * * @return array A list of contributed plugins installed. */ protected function get_contrib_list() { return array_map(function($plugins) { return array_filter($plugins, function($plugindata) { return !$plugindata->is_standard(); }); }, \core_plugin_manager::instance()->get_plugins()); } } classes/local/helper.php 0000644 00000022427 15152701722 0011274 0 ustar 00 <?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/>. /** * Collection of helper functions for the data privacy tool. * * @package tool_dataprivacy * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\local; defined('MOODLE_INTERNAL') || die(); use coding_exception; use moodle_exception; use tool_dataprivacy\api; use tool_dataprivacy\data_request; /** * Class containing helper functions for the data privacy tool. * * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class helper { /** The default number of results to be shown per page. */ const DEFAULT_PAGE_SIZE = 20; /** Filter constant associated with the request type filter. */ const FILTER_TYPE = 1; /** Filter constant associated with the request status filter. */ const FILTER_STATUS = 2; /** Filter constant associated with the request creation filter. */ const FILTER_CREATION = 3; /** The request filters preference key. */ const PREF_REQUEST_FILTERS = 'tool_dataprivacy_request-filters'; /** The number of data request records per page preference key. */ const PREF_REQUEST_PERPAGE = 'tool_dataprivacy_request-perpage'; /** * Retrieves the human-readable text value of a data request type. * * @param int $requesttype The request type. * @return string * @throws coding_exception * @throws moodle_exception */ public static function get_request_type_string($requesttype) { $types = self::get_request_types(); if (!isset($types[$requesttype])) { throw new moodle_exception('errorinvalidrequesttype', 'tool_dataprivacy'); } return $types[$requesttype]; } /** * Retrieves the human-readable shortened text value of a data request type. * * @param int $requesttype The request type. * @return string * @throws coding_exception * @throws moodle_exception */ public static function get_shortened_request_type_string($requesttype) { $types = self::get_request_types_short(); if (!isset($types[$requesttype])) { throw new moodle_exception('errorinvalidrequesttype', 'tool_dataprivacy'); } return $types[$requesttype]; } /** * Returns the key value-pairs of request type code and their string value. * * @return array */ public static function get_request_types() { return [ api::DATAREQUEST_TYPE_EXPORT => get_string('requesttypeexport', 'tool_dataprivacy'), api::DATAREQUEST_TYPE_DELETE => get_string('requesttypedelete', 'tool_dataprivacy'), api::DATAREQUEST_TYPE_OTHERS => get_string('requesttypeothers', 'tool_dataprivacy'), ]; } /** * Returns the key value-pairs of request type code and their shortened string value. * * @return array */ public static function get_request_types_short() { return [ api::DATAREQUEST_TYPE_EXPORT => get_string('requesttypeexportshort', 'tool_dataprivacy'), api::DATAREQUEST_TYPE_DELETE => get_string('requesttypedeleteshort', 'tool_dataprivacy'), api::DATAREQUEST_TYPE_OTHERS => get_string('requesttypeothersshort', 'tool_dataprivacy'), ]; } /** * Retrieves the human-readable value of a data request status. * * @param int $status The request status. * @return string * @throws moodle_exception */ public static function get_request_status_string($status) { $statuses = self::get_request_statuses(); if (!isset($statuses[$status])) { throw new moodle_exception('errorinvalidrequeststatus', 'tool_dataprivacy'); } return $statuses[$status]; } /** * Returns the key value-pairs of request status code and string value. * * @return array */ public static function get_request_statuses() { return [ api::DATAREQUEST_STATUS_PENDING => get_string('statuspending', 'tool_dataprivacy'), api::DATAREQUEST_STATUS_AWAITING_APPROVAL => get_string('statusawaitingapproval', 'tool_dataprivacy'), api::DATAREQUEST_STATUS_APPROVED => get_string('statusapproved', 'tool_dataprivacy'), api::DATAREQUEST_STATUS_PROCESSING => get_string('statusprocessing', 'tool_dataprivacy'), api::DATAREQUEST_STATUS_COMPLETE => get_string('statuscomplete', 'tool_dataprivacy'), api::DATAREQUEST_STATUS_DOWNLOAD_READY => get_string('statusready', 'tool_dataprivacy'), api::DATAREQUEST_STATUS_EXPIRED => get_string('statusexpired', 'tool_dataprivacy'), api::DATAREQUEST_STATUS_CANCELLED => get_string('statuscancelled', 'tool_dataprivacy'), api::DATAREQUEST_STATUS_REJECTED => get_string('statusrejected', 'tool_dataprivacy'), api::DATAREQUEST_STATUS_DELETED => get_string('statusdeleted', 'tool_dataprivacy'), ]; } /** * Retrieves the human-readable value of a data request creation method. * * @param int $creation The request creation method. * @return string * @throws moodle_exception */ public static function get_request_creation_method_string($creation) { $creationmethods = self::get_request_creation_methods(); if (!isset($creationmethods[$creation])) { throw new moodle_exception('errorinvalidrequestcreationmethod', 'tool_dataprivacy'); } return $creationmethods[$creation]; } /** * Returns the key value-pairs of request creation method code and string value. * * @return array */ public static function get_request_creation_methods() { return [ data_request::DATAREQUEST_CREATION_MANUAL => get_string('creationmanual', 'tool_dataprivacy'), data_request::DATAREQUEST_CREATION_AUTO => get_string('creationauto', 'tool_dataprivacy'), ]; } /** * Get the users that a user can make data request for. * * E.g. User having a parent role and has the 'tool/dataprivacy:makedatarequestsforchildren' capability. * @param int $userid The user's ID. * @return array */ public static function get_children_of_user($userid) { global $DB; // Get users that the user has role assignments to. $userfieldsapi = \core_user\fields::for_name(); $allusernames = $userfieldsapi->get_sql('u', false, '', '', false)->selects; $sql = "SELECT u.id, $allusernames FROM {role_assignments} ra, {context} c, {user} u WHERE ra.userid = :userid AND ra.contextid = c.id AND c.instanceid = u.id AND c.contextlevel = :contextlevel"; $params = [ 'userid' => $userid, 'contextlevel' => CONTEXT_USER ]; // The final list of users that we will return. $finalresults = []; // Our prospective list of users. if ($candidates = $DB->get_records_sql($sql, $params)) { foreach ($candidates as $key => $child) { $childcontext = \context_user::instance($child->id); if (has_capability('tool/dataprivacy:makedatarequestsforchildren', $childcontext, $userid)) { $finalresults[$key] = $child; } } } return $finalresults; } /** * Get options for the data requests filter. * * @return array * @throws coding_exception */ public static function get_request_filter_options() { $filters = [ self::FILTER_TYPE => (object)[ 'name' => get_string('requesttype', 'tool_dataprivacy'), 'options' => self::get_request_types_short() ], self::FILTER_STATUS => (object)[ 'name' => get_string('requeststatus', 'tool_dataprivacy'), 'options' => self::get_request_statuses() ], self::FILTER_CREATION => (object)[ 'name' => get_string('requestcreation', 'tool_dataprivacy'), 'options' => self::get_request_creation_methods() ], ]; $options = []; foreach ($filters as $category => $filtercategory) { foreach ($filtercategory->options as $key => $name) { $option = (object)[ 'category' => $filtercategory->name, 'name' => $name ]; $options["{$category}:{$key}"] = get_string('filteroption', 'tool_dataprivacy', $option); } } return $options; } } classes/filtered_userlist.php 0000644 00000004450 15152701722 0012447 0 ustar 00 <?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/>. /** * An implementation of a userlist which has been filtered and approved. * * @package tool_dataprivacy * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy; defined('MOODLE_INTERNAL') || die(); /** * An implementation of a userlist which can be filtered by role. * * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class filtered_userlist extends \core_privacy\local\request\approved_userlist { /** * Apply filters to only remove users in the expireduserids list, and to remove any who are in the unexpired list. * The unexpired list wins where a user is in both lists. * * @param int[] $expireduserids The list of userids for users who should be expired. * @param int[] $unexpireduserids The list of userids for those users who should not be expired. * @return $this */ public function apply_expired_context_filters(array $expireduserids, array $unexpireduserids) : filtered_userlist { // The current userlist content. $userids = $this->get_userids(); if (!empty($expireduserids)) { // Now remove any not on the list of expired users. $userids = array_intersect($userids, $expireduserids); } if (!empty($unexpireduserids)) { // Remove any on the list of unexpiredusers users. $userids = array_diff($userids, $unexpireduserids); } $this->set_userids($userids); return $this; } } classes/expired_context.php 0000644 00000025742 15152701722 0012132 0 ustar 00 <?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/>. /** * Class that represents an expired context. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy; use dml_exception; defined('MOODLE_INTERNAL') || die(); /** * Class that represents an expired context. * * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class expired_context extends \core\persistent { /** * Database table. */ const TABLE = 'tool_dataprivacy_ctxexpired'; /** * Expired contexts with no delete action scheduled. */ const STATUS_EXPIRED = 0; /** * Expired contexts approved for deletion. */ const STATUS_APPROVED = 1; /** * Already processed expired contexts. */ const STATUS_CLEANED = 2; /** * Return the definition of the properties of this model. * * @return array */ protected static function define_properties() { return [ 'contextid' => [ 'type' => PARAM_INT, 'description' => 'The context id.', ], 'defaultexpired' => [ 'type' => PARAM_INT, 'description' => 'Whether to default retention period for the purpose has been reached', 'default' => 1, ], 'expiredroles' => [ 'type' => PARAM_TEXT, 'description' => 'This list of roles to include during deletion', 'default' => '', ], 'unexpiredroles' => [ 'type' => PARAM_TEXT, 'description' => 'This list of roles to exclude during deletion', 'default' => '', ], 'status' => [ 'choices' => [ self::STATUS_EXPIRED, self::STATUS_APPROVED, self::STATUS_CLEANED, ], 'type' => PARAM_INT, 'description' => 'The deletion status of the context.', ], ]; } /** * Returns expired_contexts instances that match the provided level and status. * * @param int $contextlevel The context level filter criterion. * @param bool $status The expired context record's status. * @param string $sort The sort column. Must match the column name in {tool_dataprivacy_ctxexpired} table * @param int $offset The query offset. * @param int $limit The query limit. * @return expired_context[] * @throws dml_exception */ public static function get_records_by_contextlevel($contextlevel = null, $status = false, $sort = 'timecreated', $offset = 0, $limit = 0) { global $DB; $sql = "SELECT expiredctx.* FROM {" . self::TABLE . "} expiredctx JOIN {context} ctx ON ctx.id = expiredctx.contextid"; $params = []; $conditions = []; if (!empty($contextlevel)) { $conditions[] = "ctx.contextlevel = :contextlevel"; $params['contextlevel'] = intval($contextlevel); } if ($status !== false) { $conditions[] = "expiredctx.status = :status"; $params['status'] = intval($status); } if (!empty($conditions)) { $sql .= ' WHERE ' . implode(' AND ', $conditions); } $sql .= " ORDER BY expiredctx.{$sort}"; $records = $DB->get_records_sql($sql, $params, $offset, $limit); // We return class instances. $instances = array(); foreach ($records as $key => $record) { $instances[$key] = new static(0, $record); } return $instances; } /** * Returns the number of expired_contexts instances that match the provided level and status. * * @param int $contextlevel * @param bool $status * @return int * @throws dml_exception */ public static function get_record_count_by_contextlevel($contextlevel = null, $status = false) { global $DB; $sql = "SELECT COUNT(1) FROM {" . self::TABLE . "} expiredctx JOIN {context} ctx ON ctx.id = expiredctx.contextid"; $conditions = []; $params = []; if (!empty($contextlevel)) { $conditions[] = "ctx.contextlevel = :contextlevel"; $params['contextlevel'] = intval($contextlevel); } if ($status !== false) { $sql .= " AND expiredctx.status = :status"; $params['status'] = intval($status); } if (!empty($conditions)) { $sql .= ' WHERE ' . implode(' AND ', $conditions); } return $DB->count_records_sql($sql, $params); } /** * Set the list of role IDs for either expiredroles, or unexpiredroles. * * @param string $field * @param int[] $roleids * @return expired_context */ protected function set_roleids_for(string $field, array $roleids) : expired_context { $roledata = json_encode($roleids); $this->raw_set($field, $roledata); return $this; } /** * Get the list of role IDs for either expiredroles, or unexpiredroles. * * @param string $field * @return int[] */ protected function get_roleids_for(string $field) { $value = $this->raw_get($field); if (empty($value)) { return []; } return json_decode($value); } /** * Set the list of unexpired role IDs. * * @param int[] $roleids * @return expired_context */ protected function set_unexpiredroles(array $roleids) : expired_context { $this->set_roleids_for('unexpiredroles', $roleids); return $this; } /** * Add a set of role IDs to the list of expired role IDs. * * @param int[] $roleids * @return expired_context */ public function add_expiredroles(array $roleids) : expired_context { $existing = $this->get('expiredroles'); $newvalue = array_merge($existing, $roleids); $this->set('expiredroles', $newvalue); return $this; } /** * Add a set of role IDs to the list of unexpired role IDs. * * @param int[] $roleids * @return unexpired_context */ public function add_unexpiredroles(array $roleids) : expired_context { $existing = $this->get('unexpiredroles'); $newvalue = array_merge($existing, $roleids); $this->set('unexpiredroles', $newvalue); return $this; } /** * Set the list of expired role IDs. * * @param int[] $roleids * @return expired_context */ protected function set_expiredroles(array $roleids) : expired_context { $this->set_roleids_for('expiredroles', $roleids); return $this; } /** * Get the list of expired role IDs. * * @return int[] */ protected function get_expiredroles() { return $this->get_roleids_for('expiredroles'); } /** * Get the list of unexpired role IDs. * * @return int[] */ protected function get_unexpiredroles() { return $this->get_roleids_for('unexpiredroles'); } /** * Create a new expired_context based on the context, and expiry_info object. * * @param \context $context * @param expiry_info $info * @param boolean $save * @return expired_context */ public static function create_from_expiry_info(\context $context, expiry_info $info, bool $save = true) : expired_context { $record = (object) [ 'contextid' => $context->id, 'status' => self::STATUS_EXPIRED, 'defaultexpired' => (int) $info->is_default_expired(), ]; $expiredcontext = new static(0, $record); $expiredcontext->set('expiredroles', $info->get_expired_roles()); $expiredcontext->set('unexpiredroles', $info->get_unexpired_roles()); if ($save) { $expiredcontext->save(); } return $expiredcontext; } /** * Update the expired_context from an expiry_info object which relates to this context. * * @param expiry_info $info * @return $this */ public function update_from_expiry_info(expiry_info $info) : expired_context { $save = false; // Compare the expiredroles. $thisexpired = $this->get('expiredroles'); $infoexpired = $info->get_expired_roles(); sort($thisexpired); sort($infoexpired); if ($infoexpired != $thisexpired) { $this->set('expiredroles', $infoexpired); $save = true; } // Compare the unexpiredroles. $thisunexpired = $this->get('unexpiredroles'); $infounexpired = $info->get_unexpired_roles(); sort($thisunexpired); sort($infounexpired); if ($infounexpired != $thisunexpired) { $this->set('unexpiredroles', $infounexpired); $save = true; } if (empty($this->get('defaultexpired')) == $info->is_default_expired()) { $this->set('defaultexpired', (int) $info->is_default_expired()); $save = true; } if ($save) { $this->set('status', self::STATUS_EXPIRED); $this->save(); } return $this; } /** * Check whether this expired_context record is in a state ready for deletion to actually take place. * * @return bool */ public function can_process_deletion() : bool { return ($this->get('status') == self::STATUS_APPROVED); } /** * Check whether this expired_context record has already been cleaned. * * @return bool */ public function is_complete() : bool { return ($this->get('status') == self::STATUS_CLEANED); } /** * Whether this context has 'fully' expired. * That is to say that the default retention period has been reached, and that there are no unexpired roles. * * @return bool */ public function is_fully_expired() : bool { return $this->get('defaultexpired') && empty($this->get('unexpiredroles')); } } classes/external/category_exporter.php 0000644 00000005015 15152701722 0014304 0 ustar 00 <?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/>. /** * Class for exporting data category. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\external; defined('MOODLE_INTERNAL') || die(); use core\external\persistent_exporter; use tool_dataprivacy\category; use tool_dataprivacy\context_instance; /** * Class for exporting field data. * * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class category_exporter extends persistent_exporter { /** * Defines the persistent class. * * @return string */ protected static function define_class() { return \tool_dataprivacy\category::class; } /** * Returns a list of objects that are related. * * @return array */ protected static function define_related() { return array( 'context' => 'context', ); } /** * Utility function that fetches a category name from the given ID. * * @param int $categoryid The category ID. Could be INHERIT (false, -1), NOT_SET (0), or the actual ID. * @return string The purpose name. */ public static function get_name($categoryid) { global $PAGE; if ($categoryid === false || $categoryid == context_instance::INHERIT) { return get_string('inherit', 'tool_dataprivacy'); } else if ($categoryid == context_instance::NOTSET) { return get_string('notset', 'tool_dataprivacy'); } else { $purpose = new category($categoryid); $output = $PAGE->get_renderer('tool_dataprivacy'); $exporter = new self($purpose, ['context' => \context_system::instance()]); $data = $exporter->export($output); return $data->name; } } } classes/external/data_request_exporter.php 0000644 00000016100 15152701722 0015145 0 ustar 00 <?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/>. /** * Class for exporting user evidence with all competencies. * * @package tool_dataprivacy * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\external; defined('MOODLE_INTERNAL') || die(); use coding_exception; use core\external\persistent_exporter; use core_user; use core_user\external\user_summary_exporter; use dml_exception; use moodle_exception; use renderer_base; use tool_dataprivacy\api; use tool_dataprivacy\data_request; use tool_dataprivacy\local\helper; /** * Class for exporting user evidence with all competencies. * * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class data_request_exporter extends persistent_exporter { /** * Class definition. * * @return string */ protected static function define_class() { return data_request::class; } /** * Related objects definition. * * @return array */ protected static function define_related() { return [ 'context' => 'context', ]; } /** * Other properties definition. * * @return array */ protected static function define_other_properties() { return [ 'foruser' => [ 'type' => user_summary_exporter::read_properties_definition(), ], 'requestedbyuser' => [ 'type' => user_summary_exporter::read_properties_definition(), 'optional' => true ], 'dpouser' => [ 'type' => user_summary_exporter::read_properties_definition(), 'optional' => true ], 'messagehtml' => [ 'type' => PARAM_RAW, 'optional' => true ], 'typename' => [ 'type' => PARAM_TEXT, ], 'typenameshort' => [ 'type' => PARAM_TEXT, ], 'statuslabel' => [ 'type' => PARAM_TEXT, ], 'statuslabelclass' => [ 'type' => PARAM_TEXT, ], 'canreview' => [ 'type' => PARAM_BOOL, 'optional' => true, 'default' => false ], 'approvedeny' => [ 'type' => PARAM_BOOL, 'optional' => true, 'default' => false ], 'canmarkcomplete' => [ 'type' => PARAM_BOOL, 'optional' => true, 'default' => false ], ]; } /** * Assign values to the defined other properties. * * @param renderer_base $output The output renderer object. * @return array * @throws coding_exception * @throws dml_exception * @throws moodle_exception */ protected function get_other_values(renderer_base $output) { $values = []; $foruserid = $this->persistent->get('userid'); $user = core_user::get_user($foruserid, '*', MUST_EXIST); $userexporter = new user_summary_exporter($user); $values['foruser'] = $userexporter->export($output); $requestedbyid = $this->persistent->get('requestedby'); if ($requestedbyid != $foruserid) { $user = core_user::get_user($requestedbyid, '*', MUST_EXIST); $userexporter = new user_summary_exporter($user); $values['requestedbyuser'] = $userexporter->export($output); } else { $values['requestedbyuser'] = $values['foruser']; } if (!empty($this->persistent->get('dpo'))) { $dpoid = $this->persistent->get('dpo'); $user = core_user::get_user($dpoid, '*', MUST_EXIST); $userexporter = new user_summary_exporter($user); $values['dpouser'] = $userexporter->export($output); } $values['messagehtml'] = text_to_html($this->persistent->get('comments')); $requesttype = $this->persistent->get('type'); $values['typename'] = helper::get_request_type_string($requesttype); $values['typenameshort'] = helper::get_shortened_request_type_string($requesttype); $values['canreview'] = false; $values['approvedeny'] = false; $values['statuslabel'] = helper::get_request_status_string($this->persistent->get('status')); switch ($this->persistent->get('status')) { case api::DATAREQUEST_STATUS_PENDING: $values['statuslabelclass'] = 'badge-info'; // Request can be manually completed for general enquiry requests. $values['canmarkcomplete'] = $requesttype == api::DATAREQUEST_TYPE_OTHERS; break; case api::DATAREQUEST_STATUS_AWAITING_APPROVAL: $values['statuslabelclass'] = 'badge-info'; // DPO can review the request once it's ready. $values['canreview'] = true; // Whether the DPO can approve or deny the request. $values['approvedeny'] = in_array($requesttype, [api::DATAREQUEST_TYPE_EXPORT, api::DATAREQUEST_TYPE_DELETE]); // If the request's type is delete, check if user have permission to approve/deny it. if ($requesttype == api::DATAREQUEST_TYPE_DELETE) { $values['approvedeny'] = api::can_create_data_deletion_request_for_other(); } break; case api::DATAREQUEST_STATUS_APPROVED: $values['statuslabelclass'] = 'badge-info'; break; case api::DATAREQUEST_STATUS_PROCESSING: $values['statuslabelclass'] = 'badge-info'; break; case api::DATAREQUEST_STATUS_COMPLETE: case api::DATAREQUEST_STATUS_DOWNLOAD_READY: case api::DATAREQUEST_STATUS_DELETED: $values['statuslabelclass'] = 'badge-success'; break; case api::DATAREQUEST_STATUS_CANCELLED: $values['statuslabelclass'] = 'badge-warning'; break; case api::DATAREQUEST_STATUS_REJECTED: $values['statuslabelclass'] = 'badge-danger'; break; case api::DATAREQUEST_STATUS_EXPIRED: $values['statuslabelclass'] = 'badge-secondary'; break; } return $values; } } classes/external/context_instance_exporter.php 0000644 00000002603 15152701722 0016037 0 ustar 00 <?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/>. /** * Class for exporting context instance. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\external; defined('MOODLE_INTERNAL') || die(); use core\external\persistent_exporter; /** * Class for exporting context instance. * * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class context_instance_exporter extends persistent_exporter { /** * Defines the persistent class. * * @return string */ protected static function define_class() { return \tool_dataprivacy\context_instance::class; } } classes/external/name_description_exporter.php 0000644 00000003410 15152701722 0016007 0 ustar 00 <?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/>. /** * Class for exporting an object containing a name and a description. * * @package tool_dataprivacy * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\external; defined('MOODLE_INTERNAL') || die(); use core\external\exporter; /** * Class that exports an object containing a name and a description. * * @copyright 2018 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class name_description_exporter extends exporter { /** * Returns a list of objects that are related. * * @return array */ protected static function define_related() { return array( 'context' => 'context', ); } /** * Return the list of properties. * * @return array */ protected static function define_properties() { return [ 'name' => [ 'type' => PARAM_TEXT, ], 'description' => [ 'type' => PARAM_TEXT, ], ]; } } classes/external/purpose_exporter.php 0000644 00000012440 15152701722 0014164 0 ustar 00 <?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/>. /** * Class for exporting data purpose. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy\external; defined('MOODLE_INTERNAL') || die(); use coding_exception; use core\external\persistent_exporter; use Exception; use renderer_base; use tool_dataprivacy\context_instance; use tool_dataprivacy\purpose; /** * Class for exporting field data. * * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class purpose_exporter extends persistent_exporter { /** * Defines the persistent class. * * @return string */ protected static function define_class() { return purpose::class; } /** * Returns a list of objects that are related. * * @return array */ protected static function define_related() { return array( 'context' => 'context', ); } /** * Return the list of additional properties. * * @return array */ protected static function define_other_properties() { return [ 'formattedretentionperiod' => [ 'type' => PARAM_TEXT ], 'formattedlawfulbases' => [ 'type' => name_description_exporter::read_properties_definition(), 'multiple' => true ], 'formattedsensitivedatareasons' => [ 'type' => name_description_exporter::read_properties_definition(), 'multiple' => true, 'optional' => true ], 'roleoverrides' => [ 'type' => PARAM_TEXT ], ]; } /** * Return other properties. * * @param renderer_base $output * @return array * @throws coding_exception * @throws Exception */ protected function get_other_values(renderer_base $output) { $values = []; $formattedbases = []; $lawfulbases = explode(',', $this->persistent->get('lawfulbases')); if (!empty($lawfulbases)) { foreach ($lawfulbases as $basis) { if (empty(trim($basis))) { continue; } $formattedbases[] = (object)[ 'name' => get_string($basis . '_name', 'tool_dataprivacy'), 'description' => get_string($basis . '_description', 'tool_dataprivacy') ]; } } $values['formattedlawfulbases'] = $formattedbases; $formattedsensitivereasons = []; $sensitivereasons = explode(',', $this->persistent->get('sensitivedatareasons') ?? ''); if (!empty($sensitivereasons)) { foreach ($sensitivereasons as $reason) { if (empty(trim($reason))) { continue; } $formattedsensitivereasons[] = (object)[ 'name' => get_string($reason . '_name', 'tool_dataprivacy'), 'description' => get_string($reason . '_description', 'tool_dataprivacy') ]; } } $values['formattedsensitivedatareasons'] = $formattedsensitivereasons; $retentionperiod = $this->persistent->get('retentionperiod'); if ($retentionperiod) { $formattedtime = \tool_dataprivacy\api::format_retention_period(new \DateInterval($retentionperiod)); } else { $formattedtime = get_string('retentionperiodnotdefined', 'tool_dataprivacy'); } $values['formattedretentionperiod'] = $formattedtime; $values['roleoverrides'] = !empty($this->persistent->get_purpose_overrides()); return $values; } /** * Utility function that fetches a purpose name from the given ID. * * @param int $purposeid The purpose ID. Could be INHERIT (false, -1), NOT_SET (0), or the actual ID. * @return string The purpose name. */ public static function get_name($purposeid) { global $PAGE; if ($purposeid === false || $purposeid == context_instance::INHERIT) { return get_string('inherit', 'tool_dataprivacy'); } else if ($purposeid == context_instance::NOTSET) { return get_string('notset', 'tool_dataprivacy'); } else { $purpose = new purpose($purposeid); $output = $PAGE->get_renderer('tool_dataprivacy'); $exporter = new self($purpose, ['context' => \context_system::instance()]); $data = $exporter->export($output); return $data->name; } } } classes/expired_contexts_manager.php 0000644 00000115125 15152701722 0014002 0 ustar 00 <?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/>. /** * Expired contexts manager. * * @package tool_dataprivacy * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_dataprivacy; use core_privacy\manager; use tool_dataprivacy\expired_context; defined('MOODLE_INTERNAL') || die(); /** * Expired contexts manager. * * @copyright 2018 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class expired_contexts_manager { /** * Number of deleted contexts for each scheduled task run. */ const DELETE_LIMIT = 200; /** @var progress_trace The log progress tracer */ protected $progresstracer = null; /** @var manager The privacy manager */ protected $manager = null; /** @var \progress_trace Trace tool for logging */ protected $trace = null; /** * Constructor for the expired_contexts_manager. * * @param \progress_trace $trace */ public function __construct(\progress_trace $trace = null) { if (null === $trace) { $trace = new \null_progress_trace(); } $this->trace = $trace; } /** * Flag expired contexts as expired. * * @return int[] The number of contexts flagged as expired for courses, and users. */ public function flag_expired_contexts() : array { $this->trace->output('Checking requirements'); if (!$this->check_requirements()) { $this->trace->output('Requirements not met. Cannot process expired retentions.', 1); return [0, 0]; } // Clear old and stale records first. $this->trace->output('Clearing obselete records.', 0); static::clear_old_records(); $this->trace->output('Done.', 1); $this->trace->output('Calculating potential course expiries.', 0); $data = static::get_nested_expiry_info_for_courses(); $coursecount = 0; $this->trace->output('Updating course expiry data.', 0); foreach ($data as $expiryrecord) { if ($this->update_from_expiry_info($expiryrecord)) { $coursecount++; } } $this->trace->output('Done.', 1); $this->trace->output('Calculating potential user expiries.', 0); $data = static::get_nested_expiry_info_for_user(); $usercount = 0; $this->trace->output('Updating user expiry data.', 0); foreach ($data as $expiryrecord) { if ($this->update_from_expiry_info($expiryrecord)) { $usercount++; } } $this->trace->output('Done.', 1); return [$coursecount, $usercount]; } /** * Clear old and stale records. */ protected static function clear_old_records() { global $DB; $sql = "SELECT dpctx.* FROM {tool_dataprivacy_ctxexpired} dpctx LEFT JOIN {context} ctx ON ctx.id = dpctx.contextid WHERE ctx.id IS NULL"; $orphaned = $DB->get_recordset_sql($sql); foreach ($orphaned as $orphan) { $expiredcontext = new expired_context(0, $orphan); $expiredcontext->delete(); } // Delete any child of a user context. $parentpath = $DB->sql_concat('ctxuser.path', "'/%'"); $params = [ 'contextuser' => CONTEXT_USER, ]; $sql = "SELECT dpctx.* FROM {tool_dataprivacy_ctxexpired} dpctx WHERE dpctx.contextid IN ( SELECT ctx.id FROM {context} ctxuser JOIN {context} ctx ON ctx.path LIKE {$parentpath} WHERE ctxuser.contextlevel = :contextuser )"; $userchildren = $DB->get_recordset_sql($sql, $params); foreach ($userchildren as $child) { $expiredcontext = new expired_context(0, $child); $expiredcontext->delete(); } } /** * Get the full nested set of expiry data relating to all contexts. * * @param string $contextpath A contexpath to restrict results to * @return \stdClass[] */ protected static function get_nested_expiry_info($contextpath = '') : array { $coursepaths = self::get_nested_expiry_info_for_courses($contextpath); $userpaths = self::get_nested_expiry_info_for_user($contextpath); return array_merge($coursepaths, $userpaths); } /** * Get the full nested set of expiry data relating to course-related contexts. * * @param string $contextpath A contexpath to restrict results to * @return \stdClass[] */ protected static function get_nested_expiry_info_for_courses($contextpath = '') : array { global $DB; $contextfields = \context_helper::get_preload_record_columns_sql('ctx'); $expiredfields = expired_context::get_sql_fields('expiredctx', 'expiredctx'); $purposefields = 'dpctx.purposeid'; $coursefields = 'ctxcourse.expirydate AS expirydate'; $fields = implode(', ', ['ctx.id', $contextfields, $expiredfields, $coursefields, $purposefields]); // We want all contexts at course-dependant levels. $parentpath = $DB->sql_concat('ctxcourse.path', "'/%'"); // This SQL query returns all course-dependant contexts (including the course context) // which course end date already passed. // This is ordered by the context path in reverse order, which will give the child nodes before any parent node. $params = [ 'contextlevel' => CONTEXT_COURSE, ]; $where = ''; if (!empty($contextpath)) { $where = "WHERE (ctx.path = :pathmatchexact OR ctx.path LIKE :pathmatchchildren)"; $params['pathmatchexact'] = $contextpath; $params['pathmatchchildren'] = "{$contextpath}/%"; } $sql = "SELECT $fields FROM {context} ctx JOIN ( SELECT c.enddate AS expirydate, subctx.path FROM {context} subctx JOIN {course} c ON subctx.contextlevel = :contextlevel AND subctx.instanceid = c.id AND c.format != 'site' ) ctxcourse ON ctx.path LIKE {$parentpath} OR ctx.path = ctxcourse.path LEFT JOIN {tool_dataprivacy_ctxinstance} dpctx ON dpctx.contextid = ctx.id LEFT JOIN {tool_dataprivacy_ctxexpired} expiredctx ON ctx.id = expiredctx.contextid {$where} ORDER BY ctx.path DESC"; return self::get_nested_expiry_info_from_sql($sql, $params); } /** * Get the full nested set of expiry data. * * @param string $contextpath A contexpath to restrict results to * @return \stdClass[] */ protected static function get_nested_expiry_info_for_user($contextpath = '') : array { global $DB; $contextfields = \context_helper::get_preload_record_columns_sql('ctx'); $expiredfields = expired_context::get_sql_fields('expiredctx', 'expiredctx'); $purposefields = 'dpctx.purposeid'; $userfields = 'u.lastaccess AS expirydate'; $fields = implode(', ', ['ctx.id', $contextfields, $expiredfields, $userfields, $purposefields]); // We want all contexts at user-dependant levels. $parentpath = $DB->sql_concat('ctxuser.path', "'/%'"); // This SQL query returns all user-dependant contexts (including the user context) // This is ordered by the context path in reverse order, which will give the child nodes before any parent node. $params = [ 'contextlevel' => CONTEXT_USER, ]; $where = ''; if (!empty($contextpath)) { $where = "AND ctx.path = :pathmatchexact"; $params['pathmatchexact'] = $contextpath; } $sql = "SELECT $fields, u.deleted AS userdeleted FROM {context} ctx JOIN {user} u ON ctx.instanceid = u.id LEFT JOIN {tool_dataprivacy_ctxinstance} dpctx ON dpctx.contextid = ctx.id LEFT JOIN {tool_dataprivacy_ctxexpired} expiredctx ON ctx.id = expiredctx.contextid WHERE ctx.contextlevel = :contextlevel {$where} ORDER BY ctx.path DESC"; return self::get_nested_expiry_info_from_sql($sql, $params); } /** * Get the full nested set of expiry data given appropriate SQL. * Only contexts which have expired will be included. * * @param string $sql The SQL used to select the nested information. * @param array $params The params required by the SQL. * @return \stdClass[] */ protected static function get_nested_expiry_info_from_sql(string $sql, array $params) : array { global $DB; $fulllist = $DB->get_recordset_sql($sql, $params); $datalist = []; $expiredcontents = []; $pathstoskip = []; $userpurpose = data_registry::get_effective_contextlevel_value(CONTEXT_USER, 'purpose'); foreach ($fulllist as $record) { \context_helper::preload_from_record($record); $context = \context::instance_by_id($record->id, false); if (!self::is_eligible_for_deletion($pathstoskip, $context)) { // We should skip this context, and therefore all of it's children. $datalist = array_filter($datalist, function($data, $path) use ($context) { // Remove any child of this context. // Technically this should never be fulfilled because the query is ordered in path DESC, but is kept // in to be certain. return (false === strpos($path, "{$context->path}/")); }, ARRAY_FILTER_USE_BOTH); if ($record->expiredctxid) { // There was previously an expired context record. // Delete it to be on the safe side. $expiredcontext = new expired_context(null, expired_context::extract_record($record, 'expiredctx')); $expiredcontext->delete(); } continue; } if ($context instanceof \context_user) { $purpose = $userpurpose; } else { $purposevalue = $record->purposeid !== null ? $record->purposeid : context_instance::NOTSET; $purpose = api::get_effective_context_purpose($context, $purposevalue); } if ($context instanceof \context_user && !empty($record->userdeleted)) { $expiryinfo = static::get_expiry_info($purpose, $record->userdeleted); } else { $expiryinfo = static::get_expiry_info($purpose, $record->expirydate); } foreach ($datalist as $path => $data) { // Merge with already-processed children. if (strpos($path, $context->path) !== 0) { continue; } $expiryinfo->merge_with_child($data->info); } $datalist[$context->path] = (object) [ 'context' => $context, 'record' => $record, 'purpose' => $purpose, 'info' => $expiryinfo, ]; } $fulllist->close(); return $datalist; } /** * Check whether the supplied context would be elible for deletion. * * @param array $pathstoskip A set of paths which should be skipped * @param \context $context * @return bool */ protected static function is_eligible_for_deletion(array &$pathstoskip, \context $context) : bool { $shouldskip = false; // Check whether any of the child contexts are ineligble. $shouldskip = !empty(array_filter($pathstoskip, function($path) use ($context) { // If any child context has already been skipped then it will appear in this list. // Since paths include parents, test if the context under test appears as the haystack in the skipped // context's needle. return false !== (strpos($context->path, $path)); })); if (!$shouldskip && $context instanceof \context_user) { $shouldskip = !self::are_user_context_dependencies_expired($context); } if ($shouldskip) { // Add this to the list of contexts to skip for parentage checks. $pathstoskip[] = $context->path; } return !$shouldskip; } /** * Deletes the expired contexts. * * @return int[] The number of deleted contexts. */ public function process_approved_deletions() : array { $this->trace->output('Checking requirements'); if (!$this->check_requirements()) { $this->trace->output('Requirements not met. Cannot process expired retentions.', 1); return [0, 0]; } $this->trace->output('Fetching all approved and expired contexts for deletion.'); $expiredcontexts = expired_context::get_records(['status' => expired_context::STATUS_APPROVED]); $this->trace->output('Done.', 1); $totalprocessed = 0; $usercount = 0; $coursecount = 0; foreach ($expiredcontexts as $expiredctx) { $context = \context::instance_by_id($expiredctx->get('contextid'), IGNORE_MISSING); if (empty($context)) { // Unable to process this request further. // We have no context to delete. $expiredctx->delete(); continue; } $this->trace->output("Deleting data for " . $context->get_context_name(), 2); if ($this->delete_expired_context($expiredctx)) { $this->trace->output("Done.", 3); if ($context instanceof \context_user) { $usercount++; } else { $coursecount++; } $totalprocessed++; if ($totalprocessed >= $this->get_delete_limit()) { break; } } } return [$coursecount, $usercount]; } /** * Deletes user data from the provided context. * * @param expired_context $expiredctx * @return \context|false */ protected function delete_expired_context(expired_context $expiredctx) { $context = \context::instance_by_id($expiredctx->get('contextid')); $this->get_progress()->output("Deleting context {$context->id} - " . $context->get_context_name(true, true)); // Update the expired_context and verify that it is still ready for deletion. $expiredctx = $this->update_expired_context($expiredctx); if (empty($expiredctx)) { $this->get_progress()->output("Context has changed since approval and is no longer pending approval. Skipping", 1); return false; } if (!$expiredctx->can_process_deletion()) { // This only happens if the record was updated after being first fetched. $this->get_progress()->output("Context has changed since approval and must be re-approved. Skipping", 1); $expiredctx->set('status', expired_context::STATUS_EXPIRED); $expiredctx->save(); return false; } $privacymanager = $this->get_privacy_manager(); if ($expiredctx->is_fully_expired()) { if ($context instanceof \context_user) { $this->delete_expired_user_context($expiredctx); } else { // This context is fully expired - that is that the default retention period has been reached, and there are // no remaining overrides. $privacymanager->delete_data_for_all_users_in_context($context); } // Mark the record as cleaned. $expiredctx->set('status', expired_context::STATUS_CLEANED); $expiredctx->save(); return $context; } // We need to find all users in the context, and delete just those who have expired. $collection = $privacymanager->get_users_in_context($context); // Apply the expired and unexpired filters to remove the users in these categories. $userassignments = $this->get_role_users_for_expired_context($expiredctx, $context); $approvedcollection = new \core_privacy\local\request\userlist_collection($context); foreach ($collection as $pendinguserlist) { $userlist = filtered_userlist::create_from_userlist($pendinguserlist); $userlist->apply_expired_context_filters($userassignments->expired, $userassignments->unexpired); if (count($userlist)) { $approvedcollection->add_userlist($userlist); } } if (count($approvedcollection)) { // Perform the deletion with the newly approved collection. $privacymanager->delete_data_for_users_in_context($approvedcollection); } // Mark the record as cleaned. $expiredctx->set('status', expired_context::STATUS_CLEANED); $expiredctx->save(); return $context; } /** * Deletes user data from the provided user context. * * @param expired_context $expiredctx */ protected function delete_expired_user_context(expired_context $expiredctx) { global $DB; $contextid = $expiredctx->get('contextid'); $context = \context::instance_by_id($contextid); $user = \core_user::get_user($context->instanceid, '*', MUST_EXIST); $privacymanager = $this->get_privacy_manager(); // Delete all child contexts of the user context. $parentpath = $DB->sql_concat('ctxuser.path', "'/%'"); $params = [ 'contextlevel' => CONTEXT_USER, 'contextid' => $expiredctx->get('contextid'), ]; $fields = \context_helper::get_preload_record_columns_sql('ctx'); $sql = "SELECT ctx.id, $fields FROM {context} ctxuser JOIN {context} ctx ON ctx.path LIKE {$parentpath} WHERE ctxuser.contextlevel = :contextlevel AND ctxuser.id = :contextid ORDER BY ctx.path DESC"; $children = $DB->get_recordset_sql($sql, $params); foreach ($children as $child) { \context_helper::preload_from_record($child); $context = \context::instance_by_id($child->id); $privacymanager->delete_data_for_all_users_in_context($context); } $children->close(); // Delete all unprotected data that the user holds. $approvedlistcollection = new \core_privacy\local\request\contextlist_collection($user->id); $contextlistcollection = $privacymanager->get_contexts_for_userid($user->id); foreach ($contextlistcollection as $contextlist) { $contextids = []; $approvedlistcollection->add_contextlist(new \core_privacy\local\request\approved_contextlist( $user, $contextlist->get_component(), $contextlist->get_contextids() )); } $privacymanager->delete_data_for_user($approvedlistcollection, $this->get_progress()); // Delete the user context. $context = \context::instance_by_id($expiredctx->get('contextid')); $privacymanager->delete_data_for_all_users_in_context($context); // This user is now fully expired - finish by deleting the user. delete_user($user); } /** * Whether end dates are required on all courses in order for a user to be expired from them. * * @return bool */ protected static function require_all_end_dates_for_user_deletion() : bool { $requireenddate = get_config('tool_dataprivacy', 'requireallenddatesforuserdeletion'); return !empty($requireenddate); } /** * Check that the requirements to start deleting contexts are satisified. * * @return bool */ protected function check_requirements() { if (!data_registry::defaults_set()) { return false; } return true; } /** * Check whether a date is beyond the specified period. * * @param string $period The Expiry Period * @param int $comparisondate The date for comparison * @return bool */ protected static function has_expired(string $period, int $comparisondate) : bool { $dt = new \DateTime(); $dt->setTimestamp($comparisondate); $dt->add(new \DateInterval($period)); return (time() >= $dt->getTimestamp()); } /** * Get the expiry info object for the specified purpose and comparison date. * * @param purpose $purpose The purpose of this context * @param int $comparisondate The date for comparison * @return expiry_info */ protected static function get_expiry_info(purpose $purpose, int $comparisondate = 0) : expiry_info { $overrides = $purpose->get_purpose_overrides(); $expiredroles = $unexpiredroles = []; if (empty($overrides)) { // There are no overrides for this purpose. if (empty($comparisondate)) { // The date is empty, therefore this context cannot be considered for automatic expiry. $defaultexpired = false; } else { $defaultexpired = static::has_expired($purpose->get('retentionperiod'), $comparisondate); } return new expiry_info($defaultexpired, $purpose->get('protected'), [], [], []); } else { $protectedroles = []; foreach ($overrides as $override) { if (static::has_expired($override->get('retentionperiod'), $comparisondate)) { // This role has expired. $expiredroles[] = $override->get('roleid'); } else { // This role has not yet expired. $unexpiredroles[] = $override->get('roleid'); if ($override->get('protected')) { $protectedroles[$override->get('roleid')] = true; } } } $defaultexpired = false; if (static::has_expired($purpose->get('retentionperiod'), $comparisondate)) { $defaultexpired = true; } if ($defaultexpired) { $expiredroles = []; } return new expiry_info($defaultexpired, $purpose->get('protected'), $expiredroles, $unexpiredroles, $protectedroles); } } /** * Update or delete the expired_context from the expiry_info object. * This function depends upon the data structure returned from get_nested_expiry_info. * * If the context is expired in any way, then an expired_context will be returned, otherwise null will be returned. * * @param \stdClass $expiryrecord * @return expired_context|null */ protected function update_from_expiry_info(\stdClass $expiryrecord) { if ($isanyexpired = $expiryrecord->info->is_any_expired()) { // The context is expired in some fashion. // Create or update as required. if ($expiryrecord->record->expiredctxid) { $expiredcontext = new expired_context(null, expired_context::extract_record($expiryrecord->record, 'expiredctx')); $expiredcontext->update_from_expiry_info($expiryrecord->info); if ($expiredcontext->is_complete()) { return null; } } else { $expiredcontext = expired_context::create_from_expiry_info($expiryrecord->context, $expiryrecord->info); } if ($expiryrecord->context instanceof \context_user) { $userassignments = $this->get_role_users_for_expired_context($expiredcontext, $expiryrecord->context); if (!empty($userassignments->unexpired)) { $expiredcontext->delete(); return null; } } return $expiredcontext; } else { // The context is not expired. if ($expiryrecord->record->expiredctxid) { // There was previously an expired context record, but it is no longer relevant. // Delete it to be on the safe side. $expiredcontext = new expired_context(null, expired_context::extract_record($expiryrecord->record, 'expiredctx')); $expiredcontext->delete(); } return null; } } /** * Update the expired context record. * * Note: You should use the return value as the provided value will be used to fetch data only. * * @param expired_context $expiredctx The record to update * @return expired_context|null */ protected function update_expired_context(expired_context $expiredctx) { // Fetch the context from the expired_context record. $context = \context::instance_by_id($expiredctx->get('contextid')); // Fetch the current nested expiry data. $expiryrecords = self::get_nested_expiry_info($context->path); if (empty($expiryrecords[$context->path])) { $expiredctx->delete(); return null; } // Refresh the record. // Note: Use the returned expiredctx. $expiredctx = $this->update_from_expiry_info($expiryrecords[$context->path]); if (empty($expiredctx)) { return null; } if (!$context instanceof \context_user) { // Where the target context is not a user, we check all children of the context. // The expiryrecords array only contains children, fetched from the get_nested_expiry_info call above. // No need to check that these _are_ children. foreach ($expiryrecords as $expiryrecord) { if ($expiryrecord->context->id === $context->id) { // This is record for the context being tested that we checked earlier. continue; } if (empty($expiryrecord->record->expiredctxid)) { // There is no expired context record for this context. // If there is no record, then this context cannot have been approved for removal. return null; } // Fetch the expired_context object for this record. // This needs to be updated from the expiry_info data too as there may be child changes to consider. $expiredcontext = new expired_context(null, expired_context::extract_record($expiryrecord->record, 'expiredctx')); $expiredcontext->update_from_expiry_info($expiryrecord->info); if (!$expiredcontext->is_complete()) { return null; } } } return $expiredctx; } /** * Get the list of actual users for the combination of expired, and unexpired roles. * * @param expired_context $expiredctx * @param \context $context * @return \stdClass */ protected function get_role_users_for_expired_context(expired_context $expiredctx, \context $context) : \stdClass { $expiredroles = $expiredctx->get('expiredroles'); $expiredroleusers = []; if (!empty($expiredroles)) { // Find the list of expired role users. $expiredroleuserassignments = get_role_users($expiredroles, $context, true, 'ra.id, u.id AS userid', 'ra.id'); $expiredroleusers = array_map(function($assignment) { return $assignment->userid; }, $expiredroleuserassignments); } $expiredroleusers = array_unique($expiredroleusers); $unexpiredroles = $expiredctx->get('unexpiredroles'); $unexpiredroleusers = []; if (!empty($unexpiredroles)) { // Find the list of unexpired role users. $unexpiredroleuserassignments = get_role_users($unexpiredroles, $context, true, 'ra.id, u.id AS userid', 'ra.id'); $unexpiredroleusers = array_map(function($assignment) { return $assignment->userid; }, $unexpiredroleuserassignments); } $unexpiredroleusers = array_unique($unexpiredroleusers); if (!$expiredctx->get('defaultexpired')) { $tofilter = get_users_roles($context, $expiredroleusers); $tofilter = array_filter($tofilter, function($userroles) use ($expiredroles) { // Each iteration contains the list of role assignment for a specific user. // All roles that the user holds must match those in the list of expired roles. foreach ($userroles as $ra) { if (false === array_search($ra->roleid, $expiredroles)) { // This role was not found in the list of assignments. return true; } } return false; }); $unexpiredroleusers = array_merge($unexpiredroleusers, array_keys($tofilter)); } return (object) [ 'expired' => $expiredroleusers, 'unexpired' => $unexpiredroleusers, ]; } /** * Determine whether the supplied context has expired. * * @param \context $context * @return bool */ public static function is_context_expired(\context $context) : bool { $parents = $context->get_parent_contexts(true); foreach ($parents as $parent) { if ($parent instanceof \context_course) { // This is a context within a course. Check whether _this context_ is expired as a function of a course. return self::is_course_context_expired($context); } if ($parent instanceof \context_user) { // This is a context within a user. Check whether the _user_ has expired. return self::are_user_context_dependencies_expired($parent); } } return false; } /** * Check whether the course has expired. * * @param \stdClass $course * @return bool */ protected static function is_course_expired(\stdClass $course) : bool { $context = \context_course::instance($course->id); return self::is_course_context_expired($context); } /** * Determine whether the supplied course-related context has expired. * Note: This is not necessarily a _course_ context, but a context which is _within_ a course. * * @param \context $context * @return bool */ protected static function is_course_context_expired(\context $context) : bool { $expiryrecords = self::get_nested_expiry_info_for_courses($context->path); return !empty($expiryrecords[$context->path]) && $expiryrecords[$context->path]->info->is_fully_expired(); } /** * Determine whether the supplied user context's dependencies have expired. * * This checks whether courses have expired, and some other check, but does not check whether the user themself has expired. * * Although this seems unusual at first, each location calling this actually checks whether the user is elgible for * deletion, irrespective if they have actually expired. * * For example, a request to delete the user only cares about course dependencies and the user's lack of expiry * should not block their own request to be deleted; whilst the expiry eligibility check has already tested for the * user being expired. * * @param \context_user $context * @return bool */ protected static function are_user_context_dependencies_expired(\context_user $context) : bool { // The context instanceid is the user's ID. if (isguestuser($context->instanceid) || is_siteadmin($context->instanceid)) { // This is an admin, or the guest and cannot expire. return false; } $courses = enrol_get_users_courses($context->instanceid, false, ['enddate']); $requireenddate = self::require_all_end_dates_for_user_deletion(); $expired = true; foreach ($courses as $course) { if (empty($course->enddate)) { // This course has no end date. if ($requireenddate) { // Course end dates are required, and this course has no end date. $expired = false; break; } // Course end dates are not required. The subsequent checks are pointless at this time so just // skip them. continue; } if ($course->enddate >= time()) { // This course is still in the future. $expired = false; break; } // This course has an end date which is in the past. if (!self::is_course_expired($course)) { // This course has not expired yet. $expired = false; break; } } return $expired; } /** * Determine whether the supplied context has expired or unprotected for the specified user. * * @param \context $context * @param \stdClass $user * @return bool */ public static function is_context_expired_or_unprotected_for_user(\context $context, \stdClass $user) : bool { // User/course contexts can't expire if no purpose is set in the system context. if (!data_registry::defaults_set()) { return false; } $parents = $context->get_parent_contexts(true); foreach ($parents as $parent) { if ($parent instanceof \context_course) { // This is a context within a course. Check whether _this context_ is expired as a function of a course. return self::is_course_context_expired_or_unprotected_for_user($context, $user); } if ($parent instanceof \context_user) { // This is a context within a user. Check whether the _user_ has expired. return self::are_user_context_dependencies_expired($parent); } } return false; } /** * Determine whether the supplied course-related context has expired, or is unprotected. * Note: This is not necessarily a _course_ context, but a context which is _within_ a course. * * @param \context $context * @param \stdClass $user * @return bool */ protected static function is_course_context_expired_or_unprotected_for_user(\context $context, \stdClass $user) { if ($context->get_course_context()->instanceid == SITEID) { // The is an activity in the site course (front page). $purpose = data_registry::get_effective_contextlevel_value(CONTEXT_SYSTEM, 'purpose'); $info = static::get_expiry_info($purpose); } else { $expiryrecords = self::get_nested_expiry_info_for_courses($context->path); $info = $expiryrecords[$context->path]->info; } if ($info->is_fully_expired()) { // This context is fully expired. return true; } // Now perform user checks. $userroles = array_map(function($assignment) { return $assignment->roleid; }, get_user_roles($context, $user->id)); $unexpiredprotectedroles = $info->get_unexpired_protected_roles(); if (!empty(array_intersect($unexpiredprotectedroles, $userroles))) { // The user holds an unexpired and protected role. return false; } $unprotectedoverriddenroles = $info->get_unprotected_overridden_roles(); $matchingroles = array_intersect($unprotectedoverriddenroles, $userroles); if (!empty($matchingroles)) { // This user has at least one overridden role which is not a protected. // However, All such roles must match. // If the user has multiple roles then all must be expired, otherwise we should fall back to the default behaviour. if (empty(array_diff($userroles, $unprotectedoverriddenroles))) { // All roles that this user holds are a combination of expired, or unprotected. return true; } } if ($info->is_default_expired()) { // If the user has no unexpired roles, and the context is expired by default then this must be expired. return true; } return !$info->is_default_protected(); } /** * Create a new instance of the privacy manager. * * @return manager */ protected function get_privacy_manager() : manager { if (null === $this->manager) { $this->manager = new manager(); $this->manager->set_observer(new \tool_dataprivacy\manager_observer()); } return $this->manager; } /** * Fetch the limit for the maximum number of contexts to delete in one session. * * @return int */ protected function get_delete_limit() : int { return self::DELETE_LIMIT; } /** * Get the progress tracer. * * @return \progress_trace */ protected function get_progress() : \progress_trace { if (null === $this->progresstracer) { $this->set_progress(new \text_progress_trace()); } return $this->progresstracer; } /** * Set a specific tracer for the task. * * @param \progress_trace $trace * @return $this */ public function set_progress(\progress_trace $trace) : expired_contexts_manager { $this->progresstracer = $trace; return $this; } } lang/en/tool_dataprivacy.php 0000644 00000074315 15152701722 0012160 0 ustar 00 <?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/>. /** * Strings for component 'tool_dataprivacy' * * @package tool_dataprivacy * @copyright 2018 onwards Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $string['pluginname'] = 'Data privacy'; $string['pluginname_help'] = 'Data privacy plugin'; $string['activitiesandresources'] = 'Activities and resources'; $string['addcategory'] = 'Add category'; $string['addnewdefaults'] = 'Add a new module default'; $string['addpurpose'] = 'Add purpose'; $string['approve'] = 'Approve'; $string['approvedrequestsubmitted'] = 'Your request has been submitted and will be processed soon.'; $string['approverequest'] = 'Approve request'; $string['automaticdatadeletionapproval'] = 'Automatic data deletion request approval'; $string['automaticdatadeletionapproval_desc'] = 'If enabled, data deletion requests are automatically approved.<br/>Note that the automatic approval will only apply to new data deletion requests with this setting enabled. Existing data deletion requests pending approval will still have to be manually approved by the privacy officer.'; $string['automaticdataexportapproval'] = 'Automatic data export request approval'; $string['automaticdataexportapproval_desc'] = 'If enabled, data export requests are automatically approved.<br/>Note that the automatic approval will only apply to new data export requests with this setting enabled. Existing data export requests pending approval will still have to be manually approved by the privacy officer.'; $string['automaticdeletionrequests'] = 'Create automatic data deletion requests'; $string['automaticdeletionrequests_desc'] = 'If enabled, a data deletion request will be created automatically for any user accounts deleted manually.'; $string['bulkapproverequests'] = 'Approve requests'; $string['bulkdenyrequests'] = 'Deny requests'; $string['cachedef_purpose'] = 'Data purposes'; $string['cachedef_purpose_overrides'] = 'Purpose overrides in the Data privacy tool'; $string['cachedef_contextlevel'] = 'Context levels purpose and category'; $string['cancelrequest'] = 'Cancel request'; $string['cancelrequestconfirmation'] = 'Do you really want cancel this data request?'; $string['cannotreset'] = 'Unable to reset this request. Only rejected requests can be reset.'; $string['categories'] = 'Categories'; $string['category'] = 'Category'; $string['category_help'] = 'A category in the data registry describes a type of data. A new category may be added, or if Inherit is selected, the data category from a higher context is applied. Contexts are (from low to high): Blocks > Activity modules > Courses > Course categories > Site.'; $string['categorycreated'] = 'Category created'; $string['categorydefault'] = 'Default category'; $string['categorydefault_help'] = 'The default category is the data category applied to any new instances. If Inherit is selected, the data category from a higher context is applied. Contexts are (from low to high): Blocks > Activity modules > Courses > Course categories > User > Site.'; $string['categorieslist'] = 'List of data categories'; $string['categoryupdated'] = 'Category updated'; $string['close'] = 'Close'; $string['compliant'] = 'Compliant'; $string['confirmapproval'] = 'Do you really want to approve this data request?'; $string['confirmbulkapproval'] = 'Do you really want to bulk approve the selected data requests?'; $string['confirmcompletion'] = 'Do you really want to mark this user enquiry as complete?'; $string['confirmcontextdeletion'] = 'Do you really want to confirm the deletion of the selected contexts? This will also delete all of the user data for their respective sub-contexts.'; $string['confirmdenial'] = 'Do you really want deny this data request?'; $string['confirmbulkdenial'] = 'Do you really want to bulk deny the selected data requests?'; $string['confirmrequestresubmit'] = 'Are you sure you wish to cancel the current {$a->type} request for {$a->username} and resubmit it?'; $string['contactdataprotectionofficer'] = 'Contact the privacy officer'; $string['contactdataprotectionofficer_desc'] = 'If enabled, users will be able to contact the privacy officer and make a data request via a link on their profile page.'; $string['contextlevelname10'] = 'Site'; $string['contextlevelname30'] = 'Users'; $string['contextlevelname40'] = 'Course categories'; $string['contextlevelname50'] = 'Courses'; $string['contextlevelname70'] = 'Activity modules'; $string['contextlevelname80'] = 'Blocks'; $string['contextpurposecategorysaved'] = 'Purpose and category saved.'; $string['contactdpoviaprivacypolicy'] = 'Please contact the privacy officer as described in the privacy policy.'; $string['createcategory'] = 'Create data category'; $string['createdeletedatarequest'] = 'Create data deletion request'; $string['createnewdatarequest'] = 'Create a new data request'; $string['createpurpose'] = 'Create data purpose'; $string['creationauto'] = 'Automatically'; $string['creationmanual'] = 'Manually'; $string['datadeletion'] = 'Data deletion'; $string['datadeletionpagehelp'] = 'Data for which the retention period has expired are listed here. Please review and confirm data deletion, which will then be executed by the "Delete expired contexts" scheduled task.'; $string['dataprivacy:makedatarequestsforchildren'] = 'Make data requests for minors'; $string['dataprivacy:managedatarequests'] = 'Manage data requests'; $string['dataprivacy:managedataregistry'] = 'Manage data registry'; $string['dataprivacy:downloadownrequest'] = 'Download your own exported data'; $string['dataprivacy:downloadallrequests'] = 'Download exported data for everyone'; $string['dataprivacy:requestdeleteforotheruser'] = 'Request data deletion on behalf of another user'; $string['dataprivacy:makedatadeletionrequestsforchildren'] = 'Request data deletion for minors'; $string['dataprivacy:requestdelete'] = 'Request data deletion for yourself'; $string['dataregistry'] = 'Data registry'; $string['dataregistryinfo'] = 'The data registry enables categories (types of data) and purposes (the reasons for processing data) to be set for all content on the site - from users and courses down to activities and blocks. For each purpose, a retention period may be set. When a retention period has expired, the data is flagged and listed for deletion, awaiting admin confirmation.'; $string['dataretentionexplanation'] = 'This summary shows the default categories and purposes for retaining user data. Certain areas may have more specific categories and purposes than those listed here.'; $string['dataretentionsummary'] = 'Data retention summary'; $string['datarequestcreatedforuser'] = 'Data request created for {$a}'; $string['datarequestcreatedfromscheduledtask'] = 'Automatically created from a scheduled task (pre-existing deleted user).'; $string['datarequestemailsubject'] = 'Data request: {$a}'; $string['datarequestcreatedupondelete'] = 'Automatically created upon user deletion.'; $string['datarequests'] = 'Data requests'; $string['datecomment'] = '[{$a->date}]: ' . PHP_EOL . ' {$a->comment}'; $string['daterequested'] = 'Date requested'; $string['daterequesteddetail'] = 'Date requested:'; $string['defaultsinfo'] = 'Default categories and purposes are applied to all new and existing instances where a value is not set.'; $string['defaultswarninginfo'] = 'Warning: Changing these defaults may affect the retention period of existing instances.'; $string['deletecategory'] = 'Delete category'; $string['deletecategorytext'] = 'Are you sure you want to delete the category \'{$a}\'?'; $string['deletedefaults'] = 'Delete defaults: {$a}'; $string['deletedefaultsconfirmation'] = 'Are you sure you want to delete the default category and purpose for {$a} modules?'; $string['deleteexpiredcontextstask'] = 'Delete expired contexts'; $string['deleteexpireddatarequeststask'] = 'Delete expired data request export files'; $string['deleteexistingdeleteduserstask'] = 'Create delete data request for pre-existing deleted users'; $string['deletemyaccount'] = 'Delete my account'; $string['deletepurpose'] = 'Delete purpose'; $string['deletepurposetext'] = 'Are you sure you want to delete the purpose \'{$a}\'?'; $string['defaultssaved'] = 'Defaults saved'; $string['deny'] = 'Deny'; $string['denyrequest'] = 'Deny request'; $string['deprecated'] = 'Deprecated'; $string['deprecatedexplanation'] = 'This plugin is using an old version of one of the privacy interfaces and should be updated.'; $string['download'] = 'Download'; $string['downloadexpireduser'] = 'Download has expired. Submit a new request if you wish to export your personal data.'; $string['dporolemapping'] = 'Privacy officer role mapping'; $string['dporolemapping_desc'] = 'The privacy officer can manage data requests. The capability tool/dataprivacy:managedatarequests must be allowed for a role to be listed as a privacy officer role mapping option.'; $string['editcategories'] = 'Edit categories'; $string['editcategory'] = 'Edit category'; $string['editcategories'] = 'Edit categories'; $string['editdefaults'] = 'Edit defaults: {$a}'; $string['editmoduledefaults'] = 'Edit module defaults'; $string['editpurpose'] = 'Edit purpose'; $string['editpurposes'] = 'Edit purposes'; $string['effectiveretentionperiodcourse'] = '{$a} (after the course end date)'; $string['effectiveretentionperioduser'] = '{$a} (since the last time the user accessed the site)'; $string['emailsalutation'] = 'Dear {$a},'; $string['errorcannotrequestdeleteforself'] = 'You don\'t have permission to create deletion request for yourself.'; $string['errorcannotrequestdeleteforother'] = 'You don\'t have permission to create deletion request for this user.'; $string['errorcannotrequestexportforself'] = 'You don\'t have permission to create export request for yourself.'; $string['errorcontactdpodisabled'] = 'The option to contact the privacy officer is disabled.'; $string['errorinvalidrequestcomments'] = 'The comments field may contain plain text only.'; $string['errorinvalidrequestcreationmethod'] = 'Invalid request creation method!'; $string['errorinvalidrequeststatus'] = 'Invalid request status!'; $string['errorinvalidrequesttype'] = 'Invalid request type!'; $string['errornocapabilitytorequestforothers'] = 'User {$a->requestedby} doesn\'t have the capability to make a data request on behalf of user {$a->userid}'; $string['errornoexpiredcontexts'] = 'There are no expired contexts to process'; $string['errorcontexthasunexpiredchildren'] = 'The context "{$a}" still has sub-contexts that have not yet expired. No contexts have been flagged for deletion.'; $string['errorrequestalreadyexists'] = 'You already have an ongoing request.'; $string['errorrequestnotfound'] = 'Request not found'; $string['errorrequestnotwaitingforapproval'] = 'The request is not awaiting approval. Either it is not yet ready or it has already been processed.'; $string['errorsendingmessagetodpo'] = 'An error was encountered while trying to send a message to {$a}.'; $string['exceptionnotificationsubject'] = 'Exception occurred while processing privacy data'; $string['exceptionnotificationbody'] = '<p>Exception occurred while calling <b>{$a->fullmethodname}</b>.<br>This means that plugin <b>{$a->component}</b> did not complete the processing of data. The following exception information may be passed on to the plugin developer:</p><pre>{$a->message}<br> {$a->backtrace}</pre>'; $string['expiredretentionperiodtask'] = 'Expired retention period'; $string['expiry'] = 'Expiry'; $string['expandplugin'] = 'Expand and collapse plugin.'; $string['expandplugintype'] = 'Expand and collapse plugin type.'; $string['explanationtitle'] = 'Icons used on this page and what they mean.'; $string['external'] = 'Additional'; $string['externalexplanation'] = 'An additional plugin installed on this site.'; $string['filteroption'] = '{$a->category}: {$a->name}'; $string['frontpagecourse'] = 'Site home course'; $string['gdpr_art_6_1_a_description'] = 'The data subject has given consent to the processing of his or her personal data for one or more specific purposes'; $string['gdpr_art_6_1_a_name'] = 'Consent (GDPR Art. 6.1(a))'; $string['gdpr_art_6_1_b_description'] = 'Processing is necessary for the performance of a contract to which the data subject is party or in order to take steps at the request of the data subject prior to entering into a contract'; $string['gdpr_art_6_1_b_name'] = 'Contract (GDPR Art. 6.1(b))'; $string['gdpr_art_6_1_c_description'] = 'Processing is necessary for compliance with a legal obligation to which the controller is subject'; $string['gdpr_art_6_1_c_name'] = 'Legal obligation (GDPR Art 6.1(c))'; $string['gdpr_art_6_1_d_description'] = 'Processing is necessary in order to protect the vital interests of the data subject or of another natural person'; $string['gdpr_art_6_1_d_name'] = 'Vital interests (GDPR Art. 6.1(d))'; $string['gdpr_art_6_1_e_description'] = 'Processing is necessary for the performance of a task carried out in the public interest or in the exercise of official authority vested in the controller'; $string['gdpr_art_6_1_e_name'] = 'Public task (GDPR Art. 6.1(e))'; $string['gdpr_art_6_1_f_description'] = 'Processing is necessary for the purposes of the legitimate interests pursued by the controller or by a third party, except where such interests are overridden by the interests or fundamental rights and freedoms of the data subject which require protection of personal data, in particular where the data subject is a child'; $string['gdpr_art_6_1_f_name'] = 'Legitimate interests (GDPR Art. 6.1(f))'; $string['gdpr_art_9_2_a_description'] = 'The data subject has given explicit consent to the processing of those personal data for one or more specified purposes, except where Union or Member State law provide that the prohibition referred to in paragraph 1 of GDPR Article 9 may not be lifted by the data subject'; $string['gdpr_art_9_2_a_name'] = 'Explicit consent (GDPR Art. 9.2(a))'; $string['gdpr_art_9_2_b_description'] = 'Processing is necessary for the purposes of carrying out the obligations and exercising specific rights of the controller or of the data subject in the field of employment and social security and social protection law in so far as it is authorised by Union or Member State law or a collective agreement pursuant to Member State law providing for appropriate safeguards for the fundamental rights and the interests of the data subject'; $string['gdpr_art_9_2_b_name'] = 'Employment and social security/protection law (GDPR Art. 9.2(b))'; $string['gdpr_art_9_2_c_description'] = 'Processing is necessary to protect the vital interests of the data subject or of another natural person where the data subject is physically or legally incapable of giving consent'; $string['gdpr_art_9_2_c_name'] = 'Protection of vital interests (GDPR Art. 9.2(c))'; $string['gdpr_art_9_2_d_description'] = 'Processing is carried out in the course of its legitimate activities with appropriate safeguards by a foundation, association or any other not-for-profit body with a political, philosophical, religious or trade-union aim and on condition that the processing relates solely to the members or to former members of the body or to persons who have regular contact with it in connection with its purposes and that the personal data are not disclosed outside that body without the consent of the data subjects'; $string['gdpr_art_9_2_d_name'] = 'Legitimate activities regarding the members/close contacts of a foundation, association or other not-for-profit body (GDPR Art. 9.2(d))'; $string['gdpr_art_9_2_e_description'] = 'Processing relates to personal data which are manifestly made public by the data subject'; $string['gdpr_art_9_2_e_name'] = 'Data made public by the data subject (GDPR Art. 9.2(e))'; $string['gdpr_art_9_2_f_description'] = 'Processing is necessary for the establishment, exercise or defence of legal claims or whenever courts are acting in their judicial capacity'; $string['gdpr_art_9_2_f_name'] = 'Legal claims and court actions (GDPR Art. 9.2(f))'; $string['gdpr_art_9_2_g_description'] = 'Processing is necessary for reasons of substantial public interest, on the basis of Union or Member State law which shall be proportionate to the aim pursued, respect the essence of the right to data protection and provide for suitable and specific measures to safeguard the fundamental rights and the interests of the data subject'; $string['gdpr_art_9_2_g_name'] = 'Substantial public interest (GDPR Art. 9.2(g))'; $string['gdpr_art_9_2_h_description'] = 'Processing is necessary for the purposes of preventive or occupational medicine, for the assessment of the working capacity of the employee, medical diagnosis, the provision of health or social care or treatment or the management of health or social care systems and services on the basis of Union or Member State law or pursuant to contract with a health professional and subject to the conditions and safeguards referred to in paragraph 3 of GDPR Article 9'; $string['gdpr_art_9_2_h_name'] = 'Medical purposes (GDPR Art. 9.2(h))'; $string['gdpr_art_9_2_i_description'] = 'Processing is necessary for reasons of public interest in the area of public health, such as protecting against serious cross-border threats to health or ensuring high standards of quality and safety of health care and of medicinal products or medical devices, on the basis of Union or Member State law which provides for suitable and specific measures to safeguard the rights and freedoms of the data subject, in particular professional secrecy'; $string['gdpr_art_9_2_i_name'] = 'Public health (GDPR Art. 9.2(i))'; $string['gdpr_art_9_2_j_description'] = 'Processing is necessary for archiving purposes in the public interest, scientific or historical research purposes or statistical purposes in accordance with Article 89(1) based on Union or Member State law which shall be proportionate to the aim pursued, respect the essence of the right to data protection and provide for suitable and specific measures to safeguard the fundamental rights and the interests of the data subject'; $string['gdpr_art_9_2_j_name'] = 'Public interest, or scientific/historical/statistical research (GDPR Art. 9.2(j))'; $string['hide'] = 'Collapse all'; $string['httpwarning'] = 'Any data downloaded from this site may not be encrypted. Please contact your system administrator and request that they install SSL on this site.'; $string['inherit'] = 'Inherit'; $string['lawfulbases'] = 'Lawful bases'; $string['lawfulbases_help'] = 'Select at least one option that will serve as the lawful basis for processing personal data. For details on these lawful bases, please see <a href="https://gdpr-info.eu/art-6-gdpr/" target="_blank">GDPR Art. 6.1</a>'; $string['markcomplete'] = 'Mark as complete'; $string['markedcomplete'] = 'Your enquiry has been marked as complete by the privacy officer.'; $string['messageprovider:contactdataprotectionofficer'] = 'Data requests'; $string['messageprovider:datarequestprocessingresults'] = 'Data request processing results'; $string['messageprovider:notifyexceptions'] = 'Data requests exceptions notifications'; $string['message'] = 'Message'; $string['messagelabel'] = 'Message:'; $string['moduleinstancename'] = '{$a->instancename} ({$a->modulename})'; $string['mypersonaldatarequests'] = 'My personal data requests'; $string['nameandparent'] = '{$a->parent} / {$a->name}'; $string['nameemail'] = '{$a->name} ({$a->email})'; $string['nchildren'] = '{$a} children'; $string['newrequest'] = 'New request'; $string['nodatarequests'] = 'There are no data requests'; $string['nodatarequestsmatchingfilter'] = 'There are no data requests matching the given filter'; $string['noactivitiestoload'] = 'No activities'; $string['noassignedroles'] = 'No assigned roles in this context'; $string['noblockstoload'] = 'No blocks'; $string['nocategories'] = 'There are no categories yet'; $string['nocoursestoload'] = 'No activities'; $string['noexpiredcontexts'] = 'This context level has no data for which the retention period has expired.'; $string['nopersonaldatarequests'] = 'You don\'t have any personal data requests'; $string['nopurposes'] = 'There are no purposes yet'; $string['nosubjectaccessrequests'] = 'There are no data requests that you need to act on'; $string['nosystemdefaults'] = 'Site purpose and category have not yet been defined.'; $string['notset'] = 'Not set (use the default value)'; $string['notyetexpired'] = '{$a} (not yet expired)'; $string['overrideinstances'] = 'Reset instances with custom values'; $string['pluginregistry'] = 'Plugin privacy registry'; $string['pluginregistrytitle'] = 'Plugin privacy compliance registry'; $string['privacy'] = 'Privacy'; $string['privacyofficeronly'] = 'Only users who are assigned a privacy officer role ({$a}) have access to this content'; $string['privacy:metadata:preference:tool_dataprivacy_request-filters'] = 'The filters currently applied to the data requests page.'; $string['privacy:metadata:preference:tool_dataprivacy_request-perpage'] = 'The number of data requests the user prefers to see on one page'; $string['privacy:metadata:purpose'] = 'Information from data purposes made for this site.'; $string['privacy:metadata:purpose:usermodified'] = 'The ID of the user who modified the purpose'; $string['privacy:metadata:request'] = 'Information from personal data requests (subject access and deletion requests) made for this site.'; $string['privacy:metadata:request:comments'] = 'Any user comments accompanying the request.'; $string['privacy:metadata:request:userid'] = 'The ID of the user to whom the request belongs'; $string['privacy:metadata:request:requestedby'] = 'The ID of the user making the request, if made on behalf of another user.'; $string['privacy:metadata:request:dpocomment'] = 'Any comments made by the site\'s privacy officer regarding the request.'; $string['privacy:metadata:request:timecreated'] = 'The timestamp indicating when the request was made by the user.'; $string['privacyrequestexpiry'] = 'Data request expiry'; $string['privacyrequestexpiry_desc'] = 'The time that approved data requests will be available for download before expiring. If set to zero, then there is no time limit.'; $string['protected'] = 'Protected'; $string['protectedlabel'] = 'The retention of this data has a higher legal precedent over a user\'s request to be forgotten. This data will only be deleted after the retention period has expired.'; $string['purpose'] = 'Purpose'; $string['purpose_help'] = 'The purpose describes the reason for processing the data. A new purpose may be added, or if Inherit is selected, the purpose from a higher context is applied. Contexts are (from low to high): Blocks > Activity modules > Courses > Course categories > User > Site.'; $string['purposecreated'] = 'Purpose created'; $string['purposedefault'] = 'Default purpose'; $string['purposedefault_help'] = 'The default purpose is the purpose which is applied to any new instances. If Inherit is selected, the purpose from a higher context is applied. Contexts are (from low to high): Blocks > Activity modules > Courses > Course categories > User > Site.'; $string['purposes'] = 'Purposes'; $string['purposeslist'] = 'List of data purposes'; $string['purposeupdated'] = 'Purpose updated'; $string['replyto'] = 'Reply to'; $string['requestactions'] = 'Actions'; $string['requestapproved'] = 'The request has been approved'; $string['requestby'] = 'Requested by'; $string['requestbydetail'] = 'Requested by:'; $string['requestcomments'] = 'Comments'; $string['requestcomments_help'] = 'This box enables you to enter any further details about your data request.'; $string['requestcreation'] = 'Creation'; $string['requestdenied'] = 'The request has been denied'; $string['requestemailintro'] = 'You have received a data request:'; $string['requestfor'] = 'User'; $string['requestmarkedcomplete'] = 'The request has been marked as complete'; $string['requestorigin'] = 'Site'; $string['requestsapproved'] = 'The requests have been approved'; $string['requestsdenied'] = 'The requests have been denied'; $string['requeststatus'] = 'Status'; $string['requestsubmitted'] = 'Your request has been submitted to the privacy officer'; $string['requesttype'] = 'Type'; $string['requesttypeuser'] = '{$a->typename} ({$a->user})'; $string['requesttype_help'] = 'Select the reason for contacting the privacy officer. Be aware that deletion of all personal data will result in you no longer being able to log in to the site.'; $string['requesttypedelete'] = 'Delete all of my personal data'; $string['requesttypedeleteshort'] = 'Delete'; $string['requesttypeexport'] = 'Export all of my personal data'; $string['requesttypeexportshort'] = 'Export'; $string['requesttypeothers'] = 'General enquiry'; $string['requesttypeothersshort'] = 'Message'; $string['requireallenddatesforuserdeletion'] = 'Consider courses without end date as active'; $string['requireallenddatesforuserdeletion_desc'] = 'When calculating user expiry, several factors are considered: * the user\'s last login time is compared against the retention period for users; and * whether the user is actively enrolled in any courses. When checking the active enrolment in a course, if the course has no end date then this setting is used to determine whether that course is considered active or not. If the course has no end date, and this setting is enabled, then the user cannot be deleted.'; $string['requiresattention'] = 'Requires attention.'; $string['requiresattentionexplanation'] = 'This plugin does not implement the Moodle privacy API. If this plugin stores any personal data it will not be able to be exported or deleted through Moodle\'s privacy system.'; $string['resubmitrequestasnew'] = 'Resubmit as new request'; $string['resubmitrequest'] = 'Resubmit {$a->type} request for {$a->username}'; $string['resubmittedrequest'] = 'The existing {$a->type} request for {$a->username} was cancelled and resubmitted'; $string['resultdeleted'] = 'You recently requested to have your account and personal data in {$a} to be deleted. This process has been completed and you will no longer be able to log in.'; $string['resultdownloadready'] = 'Your copy of your personal data from {$a} that you recently requested is now available for download from the following link.'; $string['reviewdata'] = 'Review data'; $string['retentionperiod'] = 'Retention period'; $string['retentionperiod_help'] = 'The retention period specifies the length of time that data should be kept for. When the retention period has expired, the data is flagged and listed for deletion, awaiting admin confirmation.'; $string['retentionperiodnotdefined'] = 'No retention period was defined'; $string['retentionperiodzero'] = 'No retention period'; $string['roleoverrides'] = 'Role overrides'; $string['selectbulkaction'] = 'Please select a bulk action.'; $string['selectdatarequests'] = 'Please select data requests.'; $string['selectuserdatarequest'] = 'Select {$a->username}\'s {$a->requesttype} data request.'; $string['send'] = 'Send'; $string['sensitivedatareasons'] = 'Sensitive personal data processing reasons'; $string['sensitivedatareasons_help'] = 'Select one or more applicable reasons that exempts the prohibition of processing sensitive personal data tied to this purpose. For more information, please see <a href="https://gdpr-info.eu/art-9-gdpr/" target="_blank">GDPR Art. 9.2</a>'; $string['setdefaults'] = 'Set defaults'; $string['showdataretentionsummary'] = 'Show data retention summary'; $string['showdataretentionsummary_desc'] = 'If enabled, a link to the data retention summary is displayed in the page footer and in users\' profiles.'; $string['statusapproved'] = 'Approved'; $string['statusawaitingapproval'] = 'Awaiting approval'; $string['statuscancelled'] = 'Cancelled'; $string['statuscomplete'] = 'Complete'; $string['statusready'] = 'Download ready'; $string['statusdeleted'] = 'Deleted'; $string['statusdetail'] = 'Status:'; $string['statusexpired'] = 'Expired'; $string['statusprocessing'] = 'Processing'; $string['statuspending'] = 'Pending'; $string['statusrejected'] = 'Rejected'; $string['subjectscope'] = 'Subject scope'; $string['subjectscope_help'] = 'The subject scope lists the roles which may be assigned in this context.'; $string['summary'] = 'Registry configuration summary'; $string['systemconfignotsetwarning'] = 'A site purpose and category have not been defined. When these are not defined, all data will be removed when processing deletion requests.'; $string['user'] = 'User'; $string['userlistnoncompliant'] = 'Userlist provider missing'; $string['userlistexplanation'] = 'This plugin has the base provider but should also implement the userlist provider for full support of privacy functionality.'; $string['viewrequest'] = 'View the request'; $string['visible'] = 'Expand all'; $string['unexpiredrolewithretention'] = '{$a->retention} (Unexpired)'; $string['expiredrolewithretention'] = '{$a->retention} (Expired)'; $string['defaultexpired'] = 'Data for all users'; $string['defaultexpiredexcept'] = 'Data for all users, except those who hold any of the following roles:<br> {$a->unexpired}'; $string['defaultunexpiredwithexceptions'] = 'Only data for users who hold any of the following roles:<br> {$a->expired} Unless they also hold any of the following roles:<br> {$a->unexpired}'; $string['defaultunexpired'] = 'Only data for users holding any of the following roles:<br> {$a->expired}'; $string['tobedeleted'] = 'Data to be deleted'; $string['addroleoverride'] = 'Add role override'; $string['roleoverride'] = 'Role override'; $string['role'] = 'Role'; $string['role_help'] = 'The role which the override should apply to.'; $string['duplicaterole'] = 'Role already specified'; $string['purposeoverview'] = 'A purpose describes the intended use and retention policy for stored data. The basis for storing and retaining that data is also described in the purpose.'; $string['roleoverrideoverview'] = 'The default retention policy can be overridden for specific user roles, allowing you to specify a longer, or a shorter, retention policy. A user is only expired when all of their roles have expired.';
| ver. 1.4 |
Github
|
.
| PHP 7.4.33 | ���֧ߧ֧�ѧ�ڧ� ����ѧߧڧ��: 0.02 |
proxy
|
phpinfo
|
���ѧ����ۧܧ�