. /** * Privacy API for mod_meetletacbfree. * * @package mod_meetletacbfree * @copyright 2026 MeetLet * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_meetletacbfree\privacy; defined('MOODLE_INTERNAL') || die(); 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; /** * Privacy subsystem implementation. * * Local personal data is mainly the activity creator (userid) and optional * per-user rows in the participants JSON. Profile fields are also sent to * Adobe Connect for SSO. */ class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\plugin\provider, \core_privacy\local\request\core_userlist_provider { /** * Describe stored / exported personal data. * * @param collection $collection * @return collection */ public static function get_metadata(collection $collection): collection { $collection->add_database_table('meetletacbfree', [ 'userid' => 'privacy:metadata:meetletacbfree:userid', 'name' => 'privacy:metadata:meetletacbfree:name', 'intro' => 'privacy:metadata:meetletacbfree:intro', 'participants' => 'privacy:metadata:meetletacbfree:participants', 'timecreated' => 'privacy:metadata:meetletacbfree:timecreated', 'timemodified' => 'privacy:metadata:meetletacbfree:timemodified', ], 'privacy:metadata:meetletacbfree'); $collection->add_database_table('meetletacbfree_meetings', [ 'groupid' => 'privacy:metadata:meetletacbfree_meetings:groupid', 'meetingscoid' => 'privacy:metadata:meetletacbfree_meetings:meetingscoid', 'urlpath' => 'privacy:metadata:meetletacbfree_meetings:urlpath', ], 'privacy:metadata:meetletacbfree_meetings'); $collection->add_database_table('meetletacbfree_recordings', [ 'name' => 'privacy:metadata:meetletacbfree_recordings:name', 'urlpath' => 'privacy:metadata:meetletacbfree_recordings:urlpath', 'jobstatus' => 'privacy:metadata:meetletacbfree_recordings:jobstatus', ], 'privacy:metadata:meetletacbfree_recordings'); $collection->add_external_location_link('adobeconnect', [ 'firstname' => 'privacy:metadata:adobeconnect:firstname', 'lastname' => 'privacy:metadata:adobeconnect:lastname', 'email' => 'privacy:metadata:adobeconnect:email', 'username' => 'privacy:metadata:adobeconnect:username', ], 'privacy:metadata:adobeconnect'); $collection->add_database_table('meetletacbfree_license', [ 'installation_id' => 'privacy:metadata:meetletacbfree_license:installation_id', 'status' => 'privacy:metadata:meetletacbfree_license:status', 'last_validated' => 'privacy:metadata:meetletacbfree_license:last_validated', ], 'privacy:metadata:meetletacbfree_license'); $collection->add_external_location_link('meetlet_registration', [ 'domain' => 'privacy:metadata:meetlet_registration:domain', 'installation_id' => 'privacy:metadata:meetlet_registration:installation_id', 'company' => 'privacy:metadata:meetlet_registration:company', 'email' => 'privacy:metadata:meetlet_registration:email', 'plugin' => 'privacy:metadata:meetlet_registration:plugin', 'plugin_release' => 'privacy:metadata:meetlet_registration:plugin_release', 'platform' => 'privacy:metadata:meetlet_registration:platform', 'platform_version' => 'privacy:metadata:meetlet_registration:platform_version', ], 'privacy:metadata:meetlet_registration'); return $collection; } /** * Contexts that hold data for a user. * * @param int $userid * @return contextlist */ public static function get_contexts_for_userid(int $userid): contextlist { $contextlist = new contextlist(); $sql = "SELECT c.id FROM {context} c JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel JOIN {modules} m ON m.id = cm.module AND m.name = :modname JOIN {meetletacbfree} a ON a.id = cm.instance WHERE a.userid = :userid"; $contextlist->add_from_sql($sql, [ 'contextlevel' => CONTEXT_MODULE, 'modname' => 'meetletacbfree', 'userid' => $userid, ]); // Per-user participant mapping rows (selectiontype = user). global $DB; $pat1 = '%"selectiontype":"user","selectionid":"' . $userid . '"%'; $pat2 = '%"selectionid":"' . $userid . '","selectiontype":"user"%'; $sql = "SELECT c.id FROM {context} c JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel JOIN {modules} m ON m.id = cm.module AND m.name = :modname JOIN {meetletacbfree} a ON a.id = cm.instance WHERE " . $DB->sql_like('a.participants', ':pat1') . " OR " . $DB->sql_like('a.participants', ':pat2'); $contextlist->add_from_sql($sql, [ 'contextlevel' => CONTEXT_MODULE, 'modname' => 'meetletacbfree', 'pat1' => $pat1, 'pat2' => $pat2, ]); return $contextlist; } /** * Users who have data in a module context. * * @param userlist $userlist */ public static function get_users_in_context(userlist $userlist): void { $context = $userlist->get_context(); if (!$context instanceof \context_module) { return; } $params = [ 'instanceid' => $context->instanceid, 'modulename' => 'meetletacbfree', ]; $sql = "SELECT a.userid FROM {course_modules} cm JOIN {modules} m ON m.id = cm.module AND m.name = :modulename JOIN {meetletacbfree} a ON a.id = cm.instance WHERE cm.id = :instanceid AND a.userid <> 0"; $userlist->add_from_sql('userid', $sql, $params); global $DB; $instance = $DB->get_record_sql( "SELECT a.participants FROM {course_modules} cm JOIN {modules} m ON m.id = cm.module AND m.name = :modulename JOIN {meetletacbfree} a ON a.id = cm.instance WHERE cm.id = :instanceid", $params ); if (!$instance || empty($instance->participants)) { return; } $rules = json_decode((string)$instance->participants, true); if (!is_array($rules)) { return; } $userids = []; foreach ($rules as $rule) { if (!is_array($rule)) { continue; } if (($rule['selectiontype'] ?? '') === 'user' && !empty($rule['selectionid'])) { $userids[] = (int)$rule['selectionid']; } } $userlist->add_users(array_values(array_unique(array_filter($userids)))); } /** * Export user data for approved module contexts. * * @param approved_contextlist $contextlist */ public static function export_user_data(approved_contextlist $contextlist): void { global $DB; $user = $contextlist->get_user(); $userid = (int)$user->id; foreach ($contextlist->get_contexts() as $context) { if ($context->contextlevel !== CONTEXT_MODULE) { continue; } $cm = get_coursemodule_from_id('meetletacbfree', $context->instanceid); if (!$cm) { continue; } $activity = $DB->get_record('meetletacbfree', ['id' => $cm->instance]); if (!$activity) { continue; } $contextdata = helper::get_context_data($context, $user); $exportdata = [ 'name' => format_string($activity->name, true, ['context' => $context]), 'intro' => format_text($activity->intro ?? '', $activity->introformat ?? FORMAT_HTML, ['context' => $context]), 'is_creator' => ((int)$activity->userid === $userid), 'timecreated' => empty($activity->timecreated) ? null : transform::datetime($activity->timecreated), 'timemodified' => empty($activity->timemodified) ? null : transform::datetime($activity->timemodified), 'adobeconnect_note' => get_string('privacy:export:adobeconnect', 'meetletacbfree'), ]; $userrules = []; $rules = json_decode((string)($activity->participants ?? ''), true); if (is_array($rules)) { foreach ($rules as $rule) { if (!is_array($rule)) { continue; } if (($rule['selectiontype'] ?? '') === 'user' && (int)($rule['selectionid'] ?? 0) === $userid) { $userrules[] = [ 'role' => (string)($rule['role'] ?? ''), ]; } } } if ($userrules) { $exportdata['participant_mappings'] = $userrules; } writer::with_context($context)->export_data([], (object)array_merge((array)$contextdata, $exportdata)); helper::export_context_files($context, $user); } } /** * Delete all user data in a module context (anonymise creator + scrub user rules). * * @param \context $context */ public static function delete_data_for_all_users_in_context(\context $context): void { global $DB; if (!$context instanceof \context_module) { return; } $cm = get_coursemodule_from_id('meetletacbfree', $context->instanceid); if (!$cm) { return; } $activity = $DB->get_record('meetletacbfree', ['id' => $cm->instance]); if (!$activity) { return; } $activity->userid = 0; $activity->participants = self::strip_user_rules((string)($activity->participants ?? ''), null); $activity->timemodified = time(); $DB->update_record('meetletacbfree', $activity); } /** * Delete one user's data in the approved contexts. * * @param approved_contextlist $contextlist */ public static function delete_data_for_user(approved_contextlist $contextlist): void { global $DB; $userid = (int)$contextlist->get_user()->id; foreach ($contextlist->get_contexts() as $context) { if ($context->contextlevel !== CONTEXT_MODULE) { continue; } $cm = get_coursemodule_from_id('meetletacbfree', $context->instanceid); if (!$cm) { continue; } $activity = $DB->get_record('meetletacbfree', ['id' => $cm->instance]); if (!$activity) { continue; } if ((int)$activity->userid === $userid) { $activity->userid = 0; } $activity->participants = self::strip_user_rules((string)($activity->participants ?? ''), $userid); $activity->timemodified = time(); $DB->update_record('meetletacbfree', $activity); } } /** * Delete data for users in a context (bulk). * * @param approved_userlist $userlist */ public static function delete_data_for_users(approved_userlist $userlist): void { global $DB; $context = $userlist->get_context(); if (!$context instanceof \context_module) { return; } $userids = $userlist->get_userids(); if (!$userids) { return; } $cm = get_coursemodule_from_id('meetletacbfree', $context->instanceid); if (!$cm) { return; } $activity = $DB->get_record('meetletacbfree', ['id' => $cm->instance]); if (!$activity) { return; } if (in_array((int)$activity->userid, $userids, true)) { $activity->userid = 0; } foreach ($userids as $userid) { $activity->participants = self::strip_user_rules((string)($activity->participants ?? ''), (int)$userid); } $activity->timemodified = time(); $DB->update_record('meetletacbfree', $activity); } /** * Remove user selection rules from participants JSON. * * @param string $json * @param int|null $userid null = remove every user-type rule * @return string */ protected static function strip_user_rules(string $json, ?int $userid): string { if ($json === '') { return $json; } $rules = json_decode($json, true); if (!is_array($rules)) { return $json; } $out = []; foreach ($rules as $rule) { if (!is_array($rule)) { continue; } if (($rule['selectiontype'] ?? '') === 'user') { if ($userid === null || (int)($rule['selectionid'] ?? 0) === $userid) { continue; } } $out[] = $rule; } return json_encode($out, JSON_UNESCAPED_UNICODE); } }