Export all user data for the specified user, in the specified contexts.

@param approved_contextlist $contextlist The approved contexts to export information for.
420309
public static function export_user_data(approved_contextlist $contextlist) {
        global $DB;

        $user = $contextlist->get_user();
        $userid = $user->id;
        $contextids = array_map(function($context) {
            return $context->id;
        }, array_filter($contextlist->get_contexts(), function($context) {
            return $context->contextlevel == CONTEXT_MODULE;
        }));

        if (empty($contextids)) {
            return;
        }

        $flushdata = function($context, $data) use ($user) {
            $contextdata = helper::get_context_data($context, $user);
            helper::export_context_files($context, $user);
            $mergeddata = array_merge((array) $contextdata, (array) $data);

            // Drop the temporary keys.
            if (array_key_exists('submissions', $mergeddata)) {
                $mergeddata['submissions'] = array_values($mergeddata['submissions']);
            }

            writer::with_context($context)->export_data([], (object) $mergeddata);
        };

        $lastctxid = null;
        $data = (object) [];
        list($sql, $params) = static::prepare_export_query($contextids, $userid);
        $recordset = $DB->get_recordset_sql($sql, $params);
        foreach ($recordset as $record) {
            if ($lastctxid && $lastctxid != $record->contextid) {
                $flushdata(context::instance_by_id($lastctxid), $data);
                $data = (object) [];
            }

            context_helper::preload_from_record($record);
            $id = ($record->istmp ? 'tmp' : 'notmp') . $record->submissionid;

            if (!isset($data->submissions)) {
                $data->submissions = [];
            }

            if (!isset($data->submissions[$id])) {
                $data->submissions[$id] = [
                    'inprogress' => transform::yesno($record->istmp),
                    'anonymousresponse' => transform::yesno($record->anonymousresponse == FEEDBACK_ANONYMOUS_YES),
                    'timemodified' => transform::datetime($record->timemodified),
                    'answers' => []
                ];
            }
            $item = static::extract_item_record_from_record($record);
            $value = static::extract_value_record_from_record($record);
            $itemobj = feedback_get_item_class($record->itemtyp);
            $data->submissions[$id]['answers'][] = [
                'question' => format_text($record->itemname, FORMAT_HTML, [
                    'context' => context::instance_by_id($record->contextid),
                    'para' => false,
                    'noclean' => true,
                ]),
                'answer' => $itemobj->get_printval($item, $value)
            ];

            $lastctxid = $record->contextid;
        }

        if (!empty($lastctxid)) {
            $flushdata(context::instance_by_id($lastctxid), $data);
        }

        $recordset->close();
    }
739170
public static function delete_data_for_user(approved_contextlist $contextlist) {
        global $DB;
        $userid = $contextlist->get_user()->id;
        $prefnames = array_reduce($contextlist->get_contexts(), function($carry, $context) {
            if ($context->contextlevel == CONTEXT_BLOCK) {
                $carry[] = "block{$context->instanceid}hidden";
                $carry[] = "docked_block_instance_{$context->instanceid}";
            }
            return $carry;
        }, []);

        if (empty($prefnames)) {
            return;
        }

        list($insql, $inparams) = $DB->get_in_or_equal($prefnames, SQL_PARAMS_NAMED);
        $sql = "userid = :userid AND name $insql";
        $params = array_merge($inparams, ['userid' => $userid]);
        $DB->delete_records_select('user_preferences', $sql, $params);
    }
135795
public static function export_user_data(approved_contextlist $contextlist) {
        global $DB;

        if (empty($contextlist->count())) {
            return;
        }
        $userid = $contextlist->get_user()->id;
        list($insql, $inparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
        $params = [
            'contextlevel' => CONTEXT_COURSE,
            'userid' => $userid
         ];
        $params += $inparams;
        $sql = "SELECT ue.id,
                       ue.status,
                       ue.timestart,
                       ue.timeend,
                       ue.timecreated,
                       ue.timemodified,
                       e.enrol,
                       ctx.id as contextid
                  FROM {user_enrolments} ue
                  JOIN {enrol} e
                    ON e.id = ue.enrolid
                   AND ue.userid = :userid
                  JOIN {context} ctx
                    ON ctx.instanceid = e.courseid
                   AND ctx.contextlevel = :contextlevel
                 WHERE ctx.id $insql
                 ORDER BY ctx.id, e.enrol";
        $data = [];
        $lastcontextid = null;
        $lastenrol = null;
        $path = [get_string('privacy:metadata:user_enrolments', 'core_enrol')];
        $flush = function($lastcontextid, $lastenrol, $data) use ($path) {
            $context = \context::instance_by_id($lastcontextid);
            writer::with_context($context)->export_related_data(
                $path,
                $lastenrol,
                (object)$data
            );
        };
        $userenrolments = $DB->get_recordset_sql($sql, $params);
        foreach ($userenrolments as $userenrolment) {
            if (($lastcontextid && $lastcontextid != $userenrolment->contextid) ||
                    ($lastenrol && $lastenrol != $userenrolment->enrol)) {
                $flush($lastcontextid, $lastenrol, $data);
                $data = [];
            }
            $data[] = (object) [
                'status' => $userenrolment->status,
                'timecreated' => transform::datetime($userenrolment->timecreated),
                'timemodified' => transform::datetime($userenrolment->timemodified),
                'timestart' => transform::datetime($userenrolment->timestart),
                'timeend' => transform::datetime($userenrolment->timeend)
            ];
            $lastcontextid = $userenrolment->contextid;
            $lastenrol = $userenrolment->enrol;
        }
        if (!empty($data)) {
            $flush($lastcontextid, $lastenrol, $data);
        }
        $userenrolments->close();
    }
544950
public static function export_user_data(approved_contextlist $contextlist) {
        global $DB;

        foreach ($contextlist as $context) {
            if ($context->contextlevel != CONTEXT_MODULE) {
                continue;
            }
            $user = $contextlist->get_user();

            $rs = $DB->get_recordset_sql('SELECT w.wikimode, s.id AS subwikiid,
                    s.groupid AS subwikigroupid, s.userid AS subwikiuserid,
                    p.id AS pageid, p.userid AS pageuserid, p.title, p.cachedcontent, p.timecreated AS pagetimecreated,
                    p.timemodified AS pagetimemodified, p.timerendered AS pagetimerendered, p.pageviews, p.readonly,
                    v.id AS versionid, v.content, v.contentformat, v.version, v.timecreated AS versiontimecreated,
                    l.id AS lockid, l.sectionname, l.lockedat
                FROM {course_modules} cm
                JOIN {wiki} w ON w.id = cm.instance
                JOIN {wiki_subwikis} s ON cm.instance = s.wikiid
                LEFT JOIN {wiki_pages} p ON p.subwikiid = s.id
                LEFT JOIN {wiki_versions} v ON v.pageid = p.id AND v.userid = :user4
                LEFT JOIN {wiki_locks} l ON l.pageid = p.id AND l.userid = :user5
                WHERE cm.id = :cmid AND (s.userid = :user1 OR p.userid = :user2 OR v.userid = :user3 OR l.userid = :user6 OR
                     EXISTS (SELECT 1 FROM {comments} com WHERE com.itemid = p.id AND com.commentarea = :commentarea
                          AND com.contextid = :ctxid AND com.userid = :user7)
                )
                ORDER BY s.id, p.id, v.id',
                ['cmid' => $context->instanceid,
                    'user1' => $user->id, 'user2' => $user->id, 'user3' => $user->id, 'user4' => $user->id,
                    'user5' => $user->id, 'user6' => $user->id, 'user7' => $user->id, 'commentarea' => 'wiki_page',
                    'ctxid' => $context->id]);

            if (!$rs->current()) {
                $rs->close();
                continue;
            }

            $subwiki = [];
            $wikimode = null;
            foreach ($rs as $record) {
                if ($wikimode === null) {
                    $wikimode = $record->wikimode;
                }
                if (!isset($subwiki[$record->subwikiid])) {
                    self::export_subwiki($user, $context, $subwiki, $wikimode);
                    $subwiki = [$record->subwikiid => [
                        'groupid' => $record->subwikigroupid,
                        'userid' => $record->subwikiuserid ? transform::user($record->subwikiuserid) : 0,
                        'pages' => []
                    ]];
                }

                if (!$record->pageid) {
                    // This is an empty individual wiki.
                    continue;
                }

                // Prepend page title with the page id to guarantee uniqueness.
                $pagetitle = format_string($record->title, true, ['context' => $context]);
                $page = $record->pageid . ' ' . $pagetitle;
                if (!isset($subwiki[$record->subwikiid]['pages'][$page])) {
                    // Export basic details about the page.
                    $subwiki[$record->subwikiid]['pages'][$page] = ['page' => [
                        'id' => $record->pageid,
                        'title' => $pagetitle,
                        'cachedcontent' => $record->cachedcontent,
                    ]];
                    if ($record->pageuserid == $user->id) {
                        // This page belongs to this user. Export all details.
                        $subwiki[$record->subwikiid]['pages'][$page]['page'] += [
                            'userid' => transform::user($user->id),
                            'timecreated' => transform::datetime($record->pagetimecreated),
                            'timemodified' => transform::datetime($record->pagetimemodified),
                            'timerendered' => transform::datetime($record->pagetimerendered),
                            'pageviews' => $record->pageviews,
                            'readonly' => $record->readonly,
                        ];

                        $subwiki[$record->subwikiid]['pages'][$page]['page']['userid'] = transform::user($user->id);
                    }
                }

                if ($record->versionid) {
                    $subwiki[$record->subwikiid]['pages'][$page]['revisions'][$record->versionid] = [
                        'content' => $record->content,
                        'contentformat' => $record->contentformat,
                        'version' => $record->version,
                        'timecreated' => transform::datetime($record->versiontimecreated)
                    ];
                }

                if ($record->lockid) {
                    $subwiki[$record->subwikiid]['pages'][$page]['locks'][$record->lockid] = [
                        'sectionname' => $record->sectionname,
                        'lockedat' => transform::datetime($record->lockedat),
                    ];
                }

            }
            self::export_subwiki($user, $context, $subwiki, $wikimode);

            if ($subwiki) {
                // Export wiki itself.
                $contextdata = helper::get_context_data($context, $user);
                helper::export_context_files($context, $user);
                writer::with_context($context)->export_data([], $contextdata);
            }

            $rs->close();
        }
    }
1064242
public static function delete_data_for_user(approved_contextlist $contextlist) {
        global $DB;

        $user = $contextlist->get_user();

        list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
        $contextparams['userid'] = $user->id;

        $sql = "SELECT * FROM {editor_atto_autosave} WHERE contextid {$contextsql}";
        $autosaves = $DB->delete_records_select('editor_atto_autosave', "userid = :userid AND contextid {$contextsql}",
                $contextparams);
    }
74329
protected static function export_user_data_lti_submissions(approved_contextlist $contextlist) {
        global $DB;

        // Filter out any contexts that are not related to modules.
        $cmids = array_reduce($contextlist->get_contexts(), function($carry, $context) {
            if ($context->contextlevel == CONTEXT_MODULE) {
                $carry[] = $context->instanceid;
            }
            return $carry;
        }, []);

        if (empty($cmids)) {
            return;
        }

        $user = $contextlist->get_user();

        // Get all the LTI activities associated with the above course modules.
        $ltiidstocmids = self::get_lti_ids_to_cmids_from_cmids($cmids);
        $ltiids = array_keys($ltiidstocmids);

        list($insql, $inparams) = $DB->get_in_or_equal($ltiids, SQL_PARAMS_NAMED);
        $params = array_merge($inparams, ['userid' => $user->id]);
        $recordset = $DB->get_recordset_select('lti_submission', "ltiid $insql AND userid = :userid", $params, 'dateupdated, id');
        self::recordset_loop_and_export($recordset, 'ltiid', [], function($carry, $record) use ($user, $ltiidstocmids) {
            $carry[] = [
                'gradepercent' => $record->gradepercent,
                'originalgrade' => $record->originalgrade,
                'datesubmitted' => transform::datetime($record->datesubmitted),
                'dateupdated' => transform::datetime($record->dateupdated)
            ];
            return $carry;
        }, function($ltiid, $data) use ($user, $ltiidstocmids) {
            $context = \context_module::instance($ltiidstocmids[$ltiid]);
            $contextdata = helper::get_context_data($context, $user);
            $finaldata = (object) array_merge((array) $contextdata, ['submissions' => $data]);
            helper::export_context_files($context, $user);
            writer::with_context($context)->export_data([], $finaldata);
        });
    }
840555
public static function delete_data_for_user(approved_contextlist $contextlist) {
        global $DB;

        $user = $contextlist->get_user();

        foreach ($contextlist as $context) {
            if ($context->contextlevel != CONTEXT_MODULE) {
                continue;
            }
            // Get the assign object.
            $assign = new \assign($context, null, null);
            $assignid = $assign->get_instance()->id;

            $submissions = $DB->get_records('assign_submission', ['assignment' => $assignid, 'userid' => $user->id]);
            foreach ($submissions as $submission) {
                $requestdata = new assign_plugin_request_data($context, $assign, $submission, [], $user);
                manager::plugintype_class_callback('assignsubmission', self::ASSIGNSUBMISSION_INTERFACE,
                        'delete_submission_for_userid', [$requestdata]);
            }

            $grades = $DB->get_records('assign_grades', ['assignment' => $assignid, 'userid' => $user->id]);
            $gradingmanager = get_grading_manager($context, 'mod_assign', 'submissions');
            $controller = $gradingmanager->get_active_controller();
            foreach ($grades as $grade) {
                $requestdata = new assign_plugin_request_data($context, $assign, $grade, [], $user);
                manager::plugintype_class_callback('assignfeedback', self::ASSIGNFEEDBACK_INTERFACE,
                        'delete_feedback_for_grade', [$requestdata]);
                // Delete advanced grading information.
                if (isset($controller)) {
                    \core_grading\privacy\provider::delete_instance_data($context, $grade->id);
                }
            }

            static::delete_overrides_for_users($assign, [$user->id]);
            $DB->delete_records('assign_user_flags', ['assignment' => $assignid, 'userid' => $user->id]);
            $DB->delete_records('assign_user_mapping', ['assignment' => $assignid, 'userid' => $user->id]);
            $DB->delete_records('assign_grades', ['assignment' => $assignid, 'userid' => $user->id]);
            $DB->delete_records('assign_submission', ['assignment' => $assignid, 'userid' => $user->id]);
        }
    }
992262
protected static function export_user_calendar_event_data(approved_contextlist $contextlist) {
        // Calendar Events can exist at Site, Course Category, Course, Course Group, User, or Course Modules contexts.
        $eventdetails = self::get_calendar_event_details_by_contextlist($contextlist);

        // Multiple Calendar Events of the same eventtype and time can exist for a context, so collate them for export.
        $eventrecords = [];
        foreach ($eventdetails as $eventdetail) {
            // Create an array key based on the contextid, eventtype, and time.
            $key = $eventdetail->contextid . $eventdetail->eventtype . $eventdetail->timestart;

            if (array_key_exists($key, $eventrecords) === false) {
                $eventrecords[$key] = [ $eventdetail ];
            } else {
                $eventrecords[$key] = array_merge($eventrecords[$key], [$eventdetail]);
            }
        }
        $eventdetails->close();

        // Export Calendar Event data.
        foreach ($eventrecords as $eventrecord) {
            $index = (count($eventrecord) > 1) ? 1 : 0;

            foreach ($eventrecord as $event) {
                // Export the events using the structure Calendar/Events/{datetime}/{eventtype}-event.json.
                $subcontexts = [
                    get_string('calendar', 'calendar'),
                    get_string('events', 'calendar'),
                    date('c', $event->timestart)
                ];
                $name = $event->eventtype . '-event';

                // Use name {eventtype}-event-{index}.json if multiple eventtypes and time exists at the same context.
                if ($index != 0) {
                    $name .= '-' . $index;
                    $index++;
                }

                $eventdetails = (object) [
                    'name' => $event->name,
                    'description' => $event->description,
                    'location' => $event->location,
                    'eventtype' => $event->eventtype,
                    'timestart' => transform::datetime($event->timestart),
                    'timeduration' => $event->timeduration
                ];

                $context = \context::instance_by_id($event->contextid);
                writer::with_context($context)->export_related_data($subcontexts, $name, $eventdetails);
            }
        }
    }
1095599
public static function export_user_data(approved_contextlist $contextlist) {
        global $DB;

        // Export oauth2 linked accounts.
        $context = \context_user::instance($contextlist->get_user()->id);
        $sql = "SELECT ll.id, ll.username, ll.email, ll.timecreated, ll.timemodified, oi.name as issuername
                FROM {auth_oauth2_linked_login} ll JOIN {oauth2_issuer} oi ON oi.id = ll.issuerid
                WHERE ll.userid = :userid";
        if ($oauth2accounts = $DB->get_records_sql($sql, ['userid' => $contextlist->get_user()->id])) {
            foreach ($oauth2accounts as $oauth2account) {
                $data = (object)[
                    'timecreated' => transform::datetime($oauth2account->timecreated),
                    'timemodified' => transform::datetime($oauth2account->timemodified),
                    'issuerid' => $oauth2account->issuername,
                    'username' => $oauth2account->username,
                    'email' => $oauth2account->email
                ];
                writer::with_context($context)->export_data([
                        get_string('privacy:metadata:auth_oauth2', 'auth_oauth2'),
                        $oauth2account->issuername
                    ], $data);
            }
        }
    }
1274022
public static function export_user_data(approved_contextlist $contextlist) {
        if (empty($contextlist)) {
            return;
        }
        foreach ($contextlist as $context) {
            if ($context->contextlevel == CONTEXT_COURSE) {
                \core_group\privacy\provider::export_groups(
                    $context,
                    'enrol_meta',
                    [get_string('pluginname', 'enrol_meta')]
                );
            }
        }
    }
151616
public static function export_user_data(approved_contextlist $contextlist) {
        global $DB;

        // Remove contexts different from COURSE_MODULE.
        $contexts = array_reduce($contextlist->get_contexts(), function($carry, $context) {
            if ($context->contextlevel == CONTEXT_MODULE) {
                $carry[] = $context->id;
            }
            return $carry;
        }, []);

        if (empty($contexts)) {
            return;
        }

        $user = $contextlist->get_user();
        $userid = $user->id;
        // Get SCORM data.
        foreach ($contexts as $contextid) {
            $context = \context::instance_by_id($contextid);
            $data = helper::get_context_data($context, $user);
            writer::with_context($context)->export_data([], $data);
            helper::export_context_files($context, $user);
        }

        // Get scoes_track data.
        list($insql, $inparams) = $DB->get_in_or_equal($contexts, SQL_PARAMS_NAMED);
        $sql = "SELECT ss.id,
                       ss.attempt,
                       ss.element,
                       ss.value,
                       ss.timemodified,
                       ctx.id as contextid
                  FROM {scorm_scoes_track} ss
                  JOIN {course_modules} cm
                    ON cm.instance = ss.scormid
                  JOIN {context} ctx
                    ON ctx.instanceid = cm.id
                 WHERE ctx.id $insql
                   AND ss.userid = :userid";
        $params = array_merge($inparams, ['userid' => $userid]);

        $alldata = [];
        $scoestracks = $DB->get_recordset_sql($sql, $params);
        foreach ($scoestracks as $track) {
            $alldata[$track->contextid][$track->attempt][] = (object)[
                    'element' => $track->element,
                    'value' => $track->value,
                    'timemodified' => transform::datetime($track->timemodified),
                ];
        }
        $scoestracks->close();

        // The scoes_track data is organised in: {Course name}/{SCORM activity name}/{My attempts}/{Attempt X}/data.json
        // where X is the attempt number.
        array_walk($alldata, function($attemptsdata, $contextid) {
            $context = \context::instance_by_id($contextid);
            array_walk($attemptsdata, function($data, $attempt) use ($context) {
                $subcontext = [
                    get_string('myattempts', 'scorm'),
                    get_string('attempt', 'scorm'). " $attempt"
                ];
                writer::with_context($context)->export_data(
                    $subcontext,
                    (object)['scoestrack' => $data]
                );
            });
        });

        // Get aicc_session data.
        $sql = "SELECT ss.id,
                       ss.scormmode,
                       ss.scormstatus,
                       ss.attempt,
                       ss.lessonstatus,
                       ss.sessiontime,
                       ss.timecreated,
                       ss.timemodified,
                       ctx.id as contextid
                  FROM {scorm_aicc_session} ss
                  JOIN {course_modules} cm
                    ON cm.instance = ss.scormid
                  JOIN {context} ctx
                    ON ctx.instanceid = cm.id
                 WHERE ctx.id $insql
                   AND ss.userid = :userid";
        $params = array_merge($inparams, ['userid' => $userid]);

        $alldata = [];
        $aiccsessions = $DB->get_recordset_sql($sql, $params);
        foreach ($aiccsessions as $aiccsession) {
            $alldata[$aiccsession->contextid][] = (object)[
                    'scormmode' => $aiccsession->scormmode,
                    'scormstatus' => $aiccsession->scormstatus,
                    'lessonstatus' => $aiccsession->lessonstatus,
                    'attempt' => $aiccsession->attempt,
                    'sessiontime' => $aiccsession->sessiontime,
                    'timecreated' => transform::datetime($aiccsession->timecreated),
                    'timemodified' => transform::datetime($aiccsession->timemodified),
                ];
        }
        $aiccsessions->close();

        // The aicc_session data is organised in: {Course name}/{SCORM activity name}/{My AICC sessions}/data.json
        // In this case, the attempt hasn't been included in the json file because it can be null.
        array_walk($alldata, function($data, $contextid) {
            $context = \context::instance_by_id($contextid);
            $subcontext = [
                get_string('myaiccsessions', 'scorm')
            ];
            writer::with_context($context)->export_data(
                $subcontext,
                (object)['sessions' => $data]
            );
        });
    }
633899
public static function export_user_data(approved_contextlist $contextlist) {
        // If the user has data, then only the CONTEXT_USER should be present so get the first context.
        $contexts = $contextlist->get_contexts();
        if (count($contexts) == 0) {
            return;
        }
        $context = reset($contexts);

        // Sanity check that context is at the user context level, then get the userid.
        if ($context->contextlevel !== CONTEXT_USER) {
            return;
        }

        // Export associated userkeys.
        \core_userkey\privacy\provider::export_userkeys($context, [], 'rss');
    }
483510
public static function export_user_data(approved_contextlist $contextlist) {
        global $DB;
        if (empty($contextlist->count())) {
            return;
        }
        $userid = $contextlist->get_user()->id;
        $contextuser = \context_user::instance($userid);
        list($insql, $inparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
        $params = [
            'userid' => $userid,
            'contextlevel' => CONTEXT_USER
         ];
        $params += $inparams;
        $sql = "SELECT me.id,
                       me.rolename,
                       me.enroltime,
                       me.enroltype,
                       mh.name as hostname,
                       mc.fullname
                  FROM {mnetservice_enrol_enrolments} me
                  JOIN {context} ctx
                    ON ctx.instanceid = me.userid
                   AND ctx.contextlevel = :contextlevel
                  JOIN {mnet_host} mh
                    ON mh.id = me.hostid
                  JOIN {mnetservice_enrol_courses} mc
                    ON mc.remoteid = me.remotecourseid
                 WHERE me.userid = :userid
                   AND ctx.id {$insql}";
        $mnetenrolments = $DB->get_records_sql($sql, $params);
        foreach ($mnetenrolments as $mnetenrolment) {
            // The core_enrol data export is organised in:
            // {User Context}/User enrolments/data.json.
            $data[] = (object) [
                'host' => $mnetenrolment->hostname,
                'remotecourseid' => $mnetenrolment->fullname,
                'rolename' => $mnetenrolment->rolename,
                'enroltime' => transform::datetime($mnetenrolment->enroltime),
                'enroltype' => $mnetenrolment->enroltype
            ];
        }
        writer::with_context($contextuser)->export_data(
                [get_string('privacy:metadata:mnetservice_enrol_enrolments', 'mnetservice_enrol')],
                (object)$data
            );
    }
404381
public static function export_user_data(approved_contextlist $contextlist) {
        global $DB;

        if (empty($contextlist->count())) {
            return;
        }

        $user = $contextlist->get_user();

        list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);

        $sql = "SELECT ltiusers.lastgrade, ltiusers.lastaccess, ltiusers.timecreated, ltitools.contextid
                  FROM {enrol_lti_users} ltiusers
                  JOIN {enrol_lti_tools} ltitools
                    ON ltiusers.toolid = ltitools.id
                  JOIN {context} ctx
                    ON ctx.id = ltitools.contextid
                 WHERE ctx.id {$contextsql}
                   AND ltiusers.userid = :userid";
        $params = $contextparams + ['userid' => $user->id];
        $ltiusers = $DB->get_recordset_sql($sql, $params);
        self::recordset_loop_and_export($ltiusers, 'contextid', [], function($carry, $record) {
            $carry[] = [
                'lastgrade' => $record->lastgrade,
                'timecreated' => transform::datetime($record->lastaccess),
                'timemodified' => transform::datetime($record->timecreated)
            ];
            return $carry;
        }, function($contextid, $data) {
            $context = \context::instance_by_id($contextid);
            $finaldata = (object) $data;
            writer::with_context($context)->export_data(['enrol_lti_users'], $finaldata);
        });
    }
1299096
public static function export_user_data(approved_contextlist $contextlist) {
        global $DB;

        $user = $contextlist->get_user();
        $userid = $user->id;
        $contexts = array_reduce($contextlist->get_contexts(), function($carry, $context) use ($userid) {
            if ($context->contextlevel == CONTEXT_COURSE) {
                $carry[$context->contextlevel][] = $context;

            } else if ($context->contextlevel == CONTEXT_USER) {
                $carry[$context->contextlevel][] = $context;

            }

            return $carry;
        }, [
            CONTEXT_USER => [],
            CONTEXT_COURSE => []
        ]);

        $rootpath = [get_string('grades', 'core_grades')];
        $relatedtomepath = array_merge($rootpath, [get_string('privacy:path:relatedtome', 'core_grades')]);

        // Export the outcomes.
        static::export_user_data_outcomes_in_contexts($contextlist);

        // Export the scales.
        static::export_user_data_scales_in_contexts($contextlist);

        // Export the historical grades which have become orphans (their grade items were deleted).
        // We place those in ther user context of the graded user.
        $userids = array_values(array_map(function($context) {
            return $context->instanceid;
        }, $contexts[CONTEXT_USER]));
        if (!empty($userids)) {

            // Export own historical grades and related ones.
            list($inuseridsql, $inuseridparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
            list($inusermodifiedsql, $inusermodifiedparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
            list($inloggedusersql, $inloggeduserparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
            $usercontext = $contexts[CONTEXT_USER];
            $gghfields = static::get_fields_sql('grade_grades_history', 'ggh', 'ggh_');
            $sql = "
                SELECT $gghfields, ctx.id as ctxid
                  FROM {grade_grades_history} ggh
                  JOIN {context} ctx
                    ON ctx.instanceid = ggh.userid
                   AND ctx.contextlevel = :userlevel
             LEFT JOIN {grade_items} gi
                    ON gi.id = ggh.itemid
                 WHERE gi.id IS NULL
                   AND (ggh.userid $inuseridsql
                    OR ggh.usermodified $inusermodifiedsql
                    OR ggh.loggeduser $inloggedusersql)
                   AND (ggh.userid = :userid1
                    OR ggh.usermodified = :userid2
                    OR ggh.loggeduser = :userid3)
              ORDER BY ggh.userid, ggh.timemodified, ggh.id";
            $params = array_merge($inuseridparams, $inusermodifiedparams, $inloggeduserparams,
                ['userid1' => $userid, 'userid2' => $userid, 'userid3' => $userid, 'userlevel' => CONTEXT_USER]);

            $deletedstr = get_string('privacy:request:unknowndeletedgradeitem', 'core_grades');
            $recordset = $DB->get_recordset_sql($sql, $params);
            static::recordset_loop_and_export($recordset, 'ctxid', [], function($carry, $record) use ($deletedstr, $userid) {
                $context = context::instance_by_id($record->ctxid);
                $gghrecord = static::extract_record($record, 'ggh_');

                // Orphan grades do not have items, so we do not recreate a grade_grade item, and we do not format grades.
                $carry[] = [
                    'name' => $deletedstr,
                    'graded_user_was_you' => transform::yesno($userid == $gghrecord->userid),
                    'grade' => $gghrecord->finalgrade,
                    'feedback' => format_text($gghrecord->feedback, $gghrecord->feedbackformat, ['context' => $context]),
                    'information' => format_text($gghrecord->information, $gghrecord->informationformat, ['context' => $context]),
                    'timemodified' => transform::datetime($gghrecord->timemodified),
                    'logged_in_user_was_you' => transform::yesno($userid == $gghrecord->loggeduser),
                    'author_of_change_was_you' => transform::yesno($userid == $gghrecord->usermodified),
                    'action' => static::transform_history_action($gghrecord->action)
                ];

                return $carry;

            }, function($ctxid, $data) use ($rootpath) {
                $context = context::instance_by_id($ctxid);
                writer::with_context($context)->export_related_data($rootpath, 'history', (object) ['grades' => $data]);
            });
        }

        // Find out the course IDs.
        $courseids = array_values(array_map(function($context) {
            return $context->instanceid;
        }, $contexts[CONTEXT_COURSE]));
        if (empty($courseids)) {
            return;
        }
        list($incoursesql, $incourseparams) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);

        // Ensure that the grades are final and do not need regrading.
        array_walk($courseids, function($courseid) {
            grade_regrade_final_grades($courseid);
        });

        // Export own grades.
        $ggfields = static::get_fields_sql('grade_grade', 'gg', 'gg_');
        $gifields = static::get_fields_sql('grade_item', 'gi', 'gi_');
        $scalefields = static::get_fields_sql('grade_scale', 'sc', 'sc_');
        $sql = "
            SELECT $ggfields, $gifields, $scalefields
              FROM {grade_grades} gg
              JOIN {grade_items} gi
                ON gi.id = gg.itemid
         LEFT JOIN {scale} sc
                ON sc.id = gi.scaleid
             WHERE gi.courseid $incoursesql
               AND gg.userid = :userid
          ORDER BY gi.courseid, gi.id, gg.id";
        $params = array_merge($incourseparams, ['userid' => $userid]);

        $recordset = $DB->get_recordset_sql($sql, $params);
        static::recordset_loop_and_export($recordset, 'gi_courseid', [], function($carry, $record) {
            $context = context_course::instance($record->gi_courseid);
            $gg = static::extract_grade_grade_from_record($record);
            $carry[] = static::transform_grade($gg, $context, false);

            return $carry;

        }, function($courseid, $data) use ($rootpath) {
            $context = context_course::instance($courseid);

            $pathtofiles = [
                get_string('grades', 'core_grades'),
                get_string('feedbackfiles', 'core_grades')
            ];
            foreach ($data as $key => $grades) {
                $gg = $grades['gradeobject'];
                writer::with_context($gg->get_context())->export_area_files($pathtofiles, GRADE_FILE_COMPONENT,
                    GRADE_FEEDBACK_FILEAREA, $gg->id);
                unset($data[$key]['gradeobject']); // Do not want to export this later.
            }

            writer::with_context($context)->export_data($rootpath, (object) ['grades' => $data]);
        });

        // Export own historical grades in courses.
        $gghfields = static::get_fields_sql('grade_grades_history', 'ggh', 'ggh_');
        $sql = "
            SELECT $gghfields, $gifields, $scalefields
              FROM {grade_grades_history} ggh
              JOIN {grade_items} gi
                ON gi.id = ggh.itemid
         LEFT JOIN {scale} sc
                ON sc.id = gi.scaleid
             WHERE gi.courseid $incoursesql
               AND ggh.userid = :userid
          ORDER BY gi.courseid, ggh.timemodified, ggh.id";
        $params = array_merge($incourseparams, ['userid' => $userid]);

        $recordset = $DB->get_recordset_sql($sql, $params);
        static::recordset_loop_and_export($recordset, 'gi_courseid', [], function($carry, $record) {
            $context = context_course::instance($record->gi_courseid);
            $gg = static::extract_grade_grade_from_record($record, true);
            $carry[] = array_merge(static::transform_grade($gg, $context, true), [
                'action' => static::transform_history_action($record->ggh_action)
            ]);
            return $carry;

        }, function($courseid, $data) use ($rootpath) {
            $context = context_course::instance($courseid);

            $pathtofiles = [
                get_string('grades', 'core_grades'),
                get_string('feedbackhistoryfiles', 'core_grades')
            ];
            foreach ($data as $key => $grades) {
                $gg = $grades['gradeobject'];
                writer::with_context($gg->get_context())->export_area_files($pathtofiles, GRADE_FILE_COMPONENT,
                    GRADE_HISTORY_FEEDBACK_FILEAREA, $gg->historyid);
                unset($data[$key]['gradeobject']); // Do not want to export this later.
            }

            writer::with_context($context)->export_related_data($rootpath, 'history', (object) ['grades' => $data]);
        });

        // Export edits of categories history.
        $sql = "
            SELECT gch.id, gch.courseid, gch.fullname, gch.timemodified, gch.action
              FROM {grade_categories_history} gch
             WHERE gch.courseid $incoursesql
               AND gch.loggeduser = :userid
          ORDER BY gch.courseid, gch.timemodified, gch.id";
        $params = array_merge($incourseparams, ['userid' => $userid]);
        $recordset = $DB->get_recordset_sql($sql, $params);
        static::recordset_loop_and_export($recordset, 'courseid', [], function($carry, $record) {
            $carry[] = [
                'name' => $record->fullname,
                'timemodified' => transform::datetime($record->timemodified),
                'logged_in_user_was_you' => transform::yesno(true),
                'action' => static::transform_history_action($record->action),
            ];
            return $carry;

        }, function($courseid, $data) use ($relatedtomepath) {
            $context = context_course::instance($courseid);
            writer::with_context($context)->export_related_data($relatedtomepath, 'categories_history',
                (object) ['modified_records' => $data]);
        });

        // Export edits of items history.
        $sql = "
            SELECT gih.id, gih.courseid, gih.itemname, gih.itemmodule, gih.iteminfo, gih.timemodified, gih.action
              FROM {grade_items_history} gih
             WHERE gih.courseid $incoursesql
               AND gih.loggeduser = :userid
          ORDER BY gih.courseid, gih.timemodified, gih.id";
        $params = array_merge($incourseparams, ['userid' => $userid]);
        $recordset = $DB->get_recordset_sql($sql, $params);
        static::recordset_loop_and_export($recordset, 'courseid', [], function($carry, $record) {
            $carry[] = [
                'name' => $record->itemname,
                'module' => $record->itemmodule,
                'info' => $record->iteminfo,
                'timemodified' => transform::datetime($record->timemodified),
                'logged_in_user_was_you' => transform::yesno(true),
                'action' => static::transform_history_action($record->action),
            ];
            return $carry;

        }, function($courseid, $data) use ($relatedtomepath) {
            $context = context_course::instance($courseid);
            writer::with_context($context)->export_related_data($relatedtomepath, 'items_history',
                (object) ['modified_records' => $data]);
        });

        // Export edits of grades in course.
        $sql = "
            SELECT $ggfields, $gifields, $scalefields
              FROM {grade_grades} gg
              JOIN {grade_items} gi
                ON gg.itemid = gi.id
         LEFT JOIN {scale} sc
                ON sc.id = gi.scaleid
             WHERE gi.courseid $incoursesql
               AND gg.userid <> :userid1    -- Our grades have already been exported.
               AND gg.usermodified = :userid2
          ORDER BY gi.courseid, gg.timemodified, gg.id";
        $params = array_merge($incourseparams, ['userid1' => $userid, 'userid2' => $userid]);
        $recordset = $DB->get_recordset_sql($sql, $params);
        static::recordset_loop_and_export($recordset, 'gi_courseid', [], function($carry, $record) {
            $context = context_course::instance($record->gi_courseid);
            $gg = static::extract_grade_grade_from_record($record);
            $carry[] = array_merge(static::transform_grade($gg, $context, false), [
                'userid' => transform::user($gg->userid),
                'created_or_modified_by_you' => transform::yesno(true),
            ]);
            return $carry;

        }, function($courseid, $data) use ($relatedtomepath) {
            $context = context_course::instance($courseid);

            $pathtofiles = [
                get_string('grades', 'core_grades'),
                get_string('feedbackfiles', 'core_grades')
            ];
            foreach ($data as $key => $grades) {
                $gg = $grades['gradeobject'];
                writer::with_context($gg->get_context())->export_area_files($pathtofiles, GRADE_FILE_COMPONENT,
                    GRADE_FEEDBACK_FILEAREA, $gg->id);
                unset($data[$key]['gradeobject']); // Do not want to export this later.
            }

            writer::with_context($context)->export_related_data($relatedtomepath, 'grades', (object) ['grades' => $data]);
        });

        // Export edits of grades history in course.
        $sql = "
            SELECT $gghfields, $gifields, $scalefields, ggh.loggeduser AS loggeduser
              FROM {grade_grades_history} ggh
              JOIN {grade_items} gi
                ON ggh.itemid = gi.id
         LEFT JOIN {scale} sc
                ON sc.id = gi.scaleid
             WHERE gi.courseid $incoursesql
               AND ggh.userid <> :userid1   -- We've already exported our history.
               AND (ggh.loggeduser = :userid2
                OR ggh.usermodified = :userid3)
          ORDER BY gi.courseid, ggh.timemodified, ggh.id";
        $params = array_merge($incourseparams, ['userid1' => $userid, 'userid2' => $userid, 'userid3' => $userid]);
        $recordset = $DB->get_recordset_sql($sql, $params);
        static::recordset_loop_and_export($recordset, 'gi_courseid', [], function($carry, $record) use ($userid) {
            $context = context_course::instance($record->gi_courseid);
            $gg = static::extract_grade_grade_from_record($record, true);
            $carry[] = array_merge(static::transform_grade($gg, $context, true), [
                'userid' => transform::user($gg->userid),
                'logged_in_user_was_you' => transform::yesno($userid == $record->loggeduser),
                'author_of_change_was_you' => transform::yesno($userid == $gg->usermodified),
                'action' => static::transform_history_action($record->ggh_action),
            ]);
            return $carry;

        }, function($courseid, $data) use ($relatedtomepath) {
            $context = context_course::instance($courseid);

            $pathtofiles = [
                get_string('grades', 'core_grades'),
                get_string('feedbackhistoryfiles', 'core_grades')
            ];
            foreach ($data as $key => $grades) {
                $gg = $grades['gradeobject'];
                writer::with_context($gg->get_context())->export_area_files($pathtofiles, GRADE_FILE_COMPONENT,
                    GRADE_HISTORY_FEEDBACK_FILEAREA, $gg->historyid);
                unset($data[$key]['gradeobject']); // Do not want to export this later.
            }

            writer::with_context($context)->export_related_data($relatedtomepath, 'grades_history',
                (object) ['modified_records' => $data]);
        });
    }
970835
public static function delete_data_for_user(approved_contextlist $contextlist) {
        global $DB;

        foreach ($contextlist as $context) {
            // Let's be super certain that we have the right information for this user here.
            if ($context->contextlevel == CONTEXT_USER && $contextlist->get_user()->id == $context->instanceid) {
                $DB->delete_records('block_recentlyaccesseditems', ['userid' => $context->instanceid]);
            }
        }
    }
765874
public static function export_user_data(approved_contextlist $contextlist) {
        global $DB;

        if (empty($contextlist->count())) {
            return;
        }

        $user = $contextlist->get_user();

        list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);

        // Values of ep.receiver_email and ep.business are already normalised to lowercase characters by PayPal,
        // therefore there is no need to use LOWER() on them in the following query.
        $sql = "SELECT ep.*
                  FROM {enrol_paypal} ep
                  JOIN {enrol} e ON ep.instanceid = e.id
                  JOIN {context} ctx ON e.courseid = ctx.instanceid AND ctx.contextlevel = :contextcourse
                  JOIN {user} u ON u.id = ep.userid OR LOWER(u.email) = ep.receiver_email OR LOWER(u.email) = ep.business
                 WHERE ctx.id {$contextsql} AND u.id = :userid
              ORDER BY e.courseid";

        $params = [
            'contextcourse' => CONTEXT_COURSE,
            'userid'        => $user->id,
            'emailuserid'   => $user->id,
        ];
        $params += $contextparams;

        // Reference to the course seen in the last iteration of the loop. By comparing this with the current record, and
        // because we know the results are ordered, we know when we've moved to the PayPal transactions for a new course
        // and therefore when we can export the complete data for the last course.
        $lastcourseid = null;

        $strtransactions = get_string('transactions', 'enrol_paypal');
        $transactions = [];
        $paypalrecords = $DB->get_recordset_sql($sql, $params);
        foreach ($paypalrecords as $paypalrecord) {
            if ($lastcourseid != $paypalrecord->courseid) {
                if (!empty($transactions)) {
                    $coursecontext = \context_course::instance($paypalrecord->courseid);
                    writer::with_context($coursecontext)->export_data(
                            [$strtransactions],
                            (object) ['transactions' => $transactions]
                    );
                }
                $transactions = [];
            }

            $transaction = (object) [
                'receiver_id'         => $paypalrecord->receiver_id,
                'item_name'           => $paypalrecord->item_name,
                'userid'              => $paypalrecord->userid,
                'memo'                => $paypalrecord->memo,
                'tax'                 => $paypalrecord->tax,
                'option_name1'        => $paypalrecord->option_name1,
                'option_selection1_x' => $paypalrecord->option_selection1_x,
                'option_name2'        => $paypalrecord->option_name2,
                'option_selection2_x' => $paypalrecord->option_selection2_x,
                'payment_status'      => $paypalrecord->payment_status,
                'pending_reason'      => $paypalrecord->pending_reason,
                'reason_code'         => $paypalrecord->reason_code,
                'txn_id'              => $paypalrecord->txn_id,
                'parent_txn_id'       => $paypalrecord->parent_txn_id,
                'payment_type'        => $paypalrecord->payment_type,
                'timeupdated'         => \core_privacy\local\request\transform::datetime($paypalrecord->timeupdated),
            ];
            if ($paypalrecord->userid == $user->id) {
                $transaction->userid = $paypalrecord->userid;
            }
            if ($paypalrecord->business == \core_text::strtolower($user->email)) {
                $transaction->business = $paypalrecord->business;
            }
            if ($paypalrecord->receiver_email == \core_text::strtolower($user->email)) {
                $transaction->receiver_email = $paypalrecord->receiver_email;
            }

            $transactions[] = $paypalrecord;

            $lastcourseid = $paypalrecord->courseid;
        }
        $paypalrecords->close();

        // The data for the last activity won't have been written yet, so make sure to write it now!
        if (!empty($transactions)) {
            $coursecontext = \context_course::instance($paypalrecord->courseid);
            writer::with_context($coursecontext)->export_data(
                    [$strtransactions],
                    (object) ['transactions' => $transactions]
            );
        }
    }