<?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 Subsystem implementation for core_question.
 *
 * @package    core_question
 * @category   privacy
 * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

namespace core_question\privacy;

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\transform;
use core_privacy\local\request\userlist;
use core_privacy\local\request\writer;

defined('MOODLE_INTERNAL') || die();

require_once($CFG->libdir . '/questionlib.php');
require_once($CFG->dirroot . '/question/format.php');
require_once($CFG->dirroot . '/question/editlib.php');
require_once($CFG->dirroot . '/question/engine/datalib.php');

/**
 * Privacy Subsystem implementation for core_question.
 *
 * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class provider implements
    // This component has data.
    // We need to return all question information where the user is
    // listed in either the question.createdby or question.modifiedby fields.
    // We may also need to fetch this informtion from individual plugins in some cases.
    // e.g. to fetch the full and other question-specific meta-data.
    \core_privacy\local\metadata\provider,

    // This is a subsysytem which provides information to core.
    \core_privacy\local\request\subsystem\provider,

    // This is a subsysytem which provides information to plugins.
    \core_privacy\local\request\subsystem\plugin_provider,

    // This plugin is capable of determining which users have data within it.
    \core_privacy\local\request\core_userlist_provider,

    // This plugin is capable of determining which users have data within it for the plugins it provides data to.
    \core_privacy\local\request\shared_userlist_provider
{

    /**
     * Describe the types of data stored by the question subsystem.
     *
     * @param   collection  $items  The collection to add metadata to.
     * @return  collection  The array of metadata
     */
    public static function get_metadata(collection $items): collection {
        // Other tables link against it.

        // The 'question_usages' table does not contain any user data.
        // The table links the but doesn't store itself.

        // The 'question_attempts' table contains data about question attempts.
        // It does not contain any user ids - these are stored by the caller.
        $items->add_database_table('question_attempts', [
            'flagged'           => 'privacy:metadata:database:question_attempts:flagged',
            'responsesummary'   => 'privacy:metadata:database:question_attempts:responsesummary',
            'timemodified'      => 'privacy:metadata:database:question_attempts:timemodified',
        ], 'privacy:metadata:database:question_attempts');;

        // The 'question_attempt_steps' table contains data about changes to the state of a question attempt.
        $items->add_database_table('question_attempt_steps', [
            'state'             => 'privacy:metadata:database:question_attempt_steps:state',
            'timecreated'       => 'privacy:metadata:database:question_attempt_steps:timecreated',
            'fraction'          => 'privacy:metadata:database:question_attempt_steps:fraction',
            'userid'            => 'privacy:metadata:database:question_attempt_steps:userid',
        ], 'privacy:metadata:database:question_attempt_steps');

        // The 'question_attempt_step_data' table contains specific all metadata for each state.
        $items->add_database_table('question_attempt_step_data', [
            'name'              => 'privacy:metadata:database:question_attempt_step_data:name',
            'value'             => 'privacy:metadata:database:question_attempt_step_data:value',
        ], 'privacy:metadata:database:question_attempt_step_data');

        // These are all part of the set of the question definition
        // The 'question' table is used to store instances of each question.
        // It contains a createdby and modifiedby which related to specific users.
        $items->add_database_table('question', [
            'name'              => 'privacy:metadata:database:question:name',
            'questiontext'      => 'privacy:metadata:database:question:questiontext',
            'generalfeedback'   => 'privacy:metadata:database:question:generalfeedback',
            'timecreated'       => 'privacy:metadata:database:question:timecreated',
            'timemodified'      => 'privacy:metadata:database:question:timemodified',
            'createdby'         => 'privacy:metadata:database:question:createdby',
            'modifiedby'        => 'privacy:metadata:database:question:modifiedby',
        ], 'privacy:metadata:database:question');

        // The 'question_answers' table is used to store the set of answers, with appropriate feedback for each question.
        // It does not contain user data.

        // The 'question_hints' table is used to store hints about the correct answer for a question.
        // It does not contain user data.

        // The 'question_categories' table contains structural information about how questions are presented in the UI.
        // It does not contain user data.

        // The 'question_statistics' table contains aggregated statistics about responses.
        // It does not contain any identifiable user data.

        $items->add_database_table('question_bank_entries', [
            'ownerid' => 'privacy:metadata:database:question_bank_entries:ownerid',
        ], 'privacy:metadata:database:question_bank_entries');

        // The question subsystem makes use of the qtype, qformat, and qbehaviour plugin types.
        $items->add_plugintype_link('qtype', [], 'privacy:metadata:link:qtype');
        $items->add_plugintype_link('qformat', [], 'privacy:metadata:link:qformat');
        $items->add_plugintype_link('qbehaviour', [], 'privacy:metadata:link:qbehaviour');

        return $items;
    }

    /**
     * Export the data for all question attempts on this question usage.
     *
     * Where a user is the owner of the usage, then the full detail of that usage will be included.
     * Where a user has been involved in the usage, but it is not their own usage, then only their specific
     * involvement will be exported.
     *
     * @param   int             $userid     The userid to export.
     * @param   \context        $context    The context that the question was used within.
     * @param   array           $usagecontext  The subcontext of this usage.
     * @param   int             $usage      The question usage ID.
     * @param   \question_display_options   $options    The display options used for formatting.
     * @param   bool            $isowner    Whether the user being exported is the user who used the question.
     */
    public static function export_question_usage(
            int $userid,
            \context $context,
            array $usagecontext,
            int $usage,
            \question_display_options $options,
            bool $isowner
        ) {
        // Determine the questions in this usage.
        $quba = \question_engine::load_questions_usage_by_activity($usage);

        $basepath = $usagecontext;
        $questionscontext = array_merge($usagecontext, [
            get_string('questions', 'core_question'),
        ]);

        foreach ($quba->get_attempt_iterator() as $qa) {
            $question = $qa->get_question(false);
            $slotno = $qa->get_slot();
            $questionnocontext = array_merge($questionscontext, [$slotno]);

            if ($isowner) {
                // This user is the overal owner of the question attempt and all data wil therefore be exported.
                //
                // Respect _some_ of the question_display_options to ensure that they don't have access to
                // generalfeedback and mark if the display options prevent this.
                // This is defensible because they can submit questions without completing a quiz and perform an SAR to
                // get prior access to the feedback and mark to improve upon it.
                // Export the response.
                $data = (object) [
                    'name' => $question->name,
                    'question' => $qa->get_question_summary(),
                    'answer' => $qa->get_response_summary(),
                    'timemodified' => transform::datetime($qa->timemodified),
                ];

                if ($options->marks >= \question_display_options::MARK_AND_MAX) {
                    $data->mark = $qa->format_mark($options->markdp);
                }

                if ($options->flags != \question_display_options::HIDDEN) {
                    $data->flagged = transform::yesno($qa->is_flagged());
                }

                if ($options->generalfeedback != \question_display_options::HIDDEN) {
                    $data->generalfeedback = $question->format_generalfeedback($qa);
                }

                if ($options->manualcomment != \question_display_options::HIDDEN) {
                    if ($qa->has_manual_comment()) {
                        // Note - the export of the step data will ensure that the files are exported.
                        // No need to do it again here.
                        list($comment, $commentformat, $step) = $qa->get_manual_comment();

                        $comment = writer::with_context($context)
                            ->rewrite_pluginfile_urls(
                                $questionnocontext,
                                'question',
                                'response_bf_comment',
                                $step->get_id(),
                                $comment
                            );
                        $data->comment = $qa->get_behaviour(false)->format_comment($comment, $commentformat);
                    }
                }

                writer::with_context($context)
                    ->export_data($questionnocontext, $data);

                // Export the step data.
                static::export_question_attempt_steps($userid, $context, $questionnocontext, $qa, $options, $isowner);
            }
        }
    }

    /**
     * Export the data for each step transition for each question in each question attempt.
     *
     * Where a user is the owner of the usage, then all steps in the question usage will be exported.
     * Where a user is not the owner, but has been involved in the usage, then only their specific
     * involvement will be exported.
     *
     * @param   int                 $userid     The user to export for
     * @param   \context            $context    The context that the question was used within.
     * @param   array               $questionnocontext  The subcontext of this question number.
     * @param   \question_attempt   $qa         The attempt being checked
     * @param   \question_display_options   $options    The display options used for formatting.
     * @param   bool                $isowner    Whether the user being exported is the user who used the question.
     */
    public static function export_question_attempt_steps(
            int $userid,
            \context $context,
            array $questionnocontext,
            \question_attempt $qa,
            \question_display_options $options,
            $isowner
        ) {
        $attemptdata = (object) [
                'steps' => [],
            ];
        $stepno = 0;
        foreach ($qa->get_step_iterator() as $i => $step) {
            $stepno++;

            if ($isowner || ($step->get_user_id() != $userid)) {
                // The user is the owner, or the author of the step.

                $restrictedqa = new \question_attempt_with_restricted_history($qa, $i, null);
                $stepdata = (object) [
                    // Note: Do not include the user here.
                    'time' => transform::datetime($step->get_timecreated()),
                    'action' => $qa->summarise_action($step),
                ];

                if ($options->marks >= \question_display_options::MARK_AND_MAX) {
                    $stepdata->mark = $qa->format_fraction_as_mark($step->get_fraction(), $options->markdp);
                }

                if ($options->correctness != \question_display_options::HIDDEN) {
                    $stepdata->state = $restrictedqa->get_state_string($options->correctness);
                }

                if ($step->has_behaviour_var('comment')) {
                    $comment = $step->get_behaviour_var('comment');
                    $commentformat = $step->get_behaviour_var('commentformat');

                    if (empty(trim($comment))) {
                        // Skip empty comments.
                        continue;
                    }

                    // Format the comment.
                    $comment = writer::with_context($context)
                        ->rewrite_pluginfile_urls(
                            $questionnocontext,
                            'question',
                            'response_bf_comment',
                            $step->get_id(),
                            $comment
                        );

                    // Export any files associated with the comment files area.
                    writer::with_context($context)
                        ->export_area_files(
                            $questionnocontext,
                            'question',
                            "response_bf_comment",
                            $step->get_id()
                        );

                    $stepdata->comment = $qa->get_behaviour(false)->format_comment($comment, $commentformat);
                }

                // Export any response files associated with this step.
                foreach (\question_engine::get_all_response_file_areas() as $filearea) {
                    writer::with_context($context)
                        ->export_area_files(
                                $questionnocontext,
                                'question',
                                $filearea,
                                $step->get_id()
                            );
                }

                $attemptdata->steps[$stepno] = $stepdata;
            }
        }

        if (!empty($attemptdata->steps)) {
            writer::with_context($context)
                ->export_related_data($questionnocontext, 'steps', $attemptdata);
        }
    }

    /**
     * Get the list of contexts where the specified user has either created, or edited a question.
     *
     * To export usage of a question, please call {@link provider::export_question_usage()} from the module which
     * instantiated the usage of the question.
     *
     * @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 {
        $contextlist = new contextlist();

        // A user may have created or updated a question.
        // Questions are linked against a question category, which has a contextid field.
        $sql = "SELECT qc.contextid
                  FROM {question} q
                  JOIN {question_versions} qv ON qv.questionid = q.id
                  JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
                  JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
                 WHERE q.createdby = :useridcreated
                       OR q.modifiedby = :useridmodified";
        $params = [
            'useridcreated' => $userid,
            'useridmodified' => $userid,
        ];
        $contextlist->add_from_sql($sql, $params);

        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();

        // A user may have created or updated a question.
        // Questions are linked against a question category, which has a contextid field.
        $sql = "SELECT q.createdby, q.modifiedby
                  FROM {question} q
                  JOIN {question_versions} qv ON qv.questionid = q.id
                  JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
                  JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
                 WHERE qc.contextid = :contextid";

        $params = [
            'contextid' => $context->id
        ];

        $userlist->add_from_sql('createdby', $sql, $params);
        $userlist->add_from_sql('modifiedby', $sql, $params);
    }

    /**
     * Determine related question usages for a user.
     *
     * @param   string          $prefix     A unique prefix to add to the table alias
     * @param   string          $component  The name of the component to fetch usages for.
     * @param   string          $joinfield  The SQL field name to use in the JOIN ON - e.g. q.usageid
     * @param   int             $userid     The user to search.
     * @return  \qubaid_join
     */
    public static function get_related_question_usages_for_user(string $prefix, string $component, string $joinfield, int $userid): \qubaid_join {
        return new \qubaid_join("
                JOIN {question_usages} {$prefix}_qu ON {$prefix}_qu.id = {$joinfield}
                 AND {$prefix}_qu.component = :{$prefix}_usagecomponent
                JOIN {question_attempts} {$prefix}_qa ON {$prefix}_qa.questionusageid = {$prefix}_qu.id
                JOIN {question_attempt_steps} {$prefix}_qas ON {$prefix}_qas.questionattemptid = {$prefix}_qa.id",
            "{$prefix}_qu.id",
            "{$prefix}_qas.userid = :{$prefix}_stepuserid",
            [
                "{$prefix}_stepuserid" => $userid,
                "{$prefix}_usagecomponent" => $component,
            ]);
    }

    /**
     * Add the list of users who have rated in the specified constraints.
     *
     * @param   userlist    $userlist   The userlist to add the users to.
     * @param   string      $prefix     A unique prefix to add to the table alias to avoid interference with your own sql.
     * @param   string      $insql      The SQL to use in a sub-select for the question_usages.id query.
     * @param   array       $params     The params required for the insql.
     * @param   int|null    $contextid  An optional context id, in case the $sql query is not already filtered by that.
     */
    public static function get_users_in_context_from_sql(userlist $userlist, string $prefix, string $insql, $params,
            int $contextid = null) {

        $sql = "SELECT {$prefix}_qas.userid
                  FROM {question_attempt_steps} {$prefix}_qas
                  JOIN {question_attempts} {$prefix}_qa ON {$prefix}_qas.questionattemptid = {$prefix}_qa.id
                  JOIN {question_usages} {$prefix}_qu ON {$prefix}_qa.questionusageid = {$prefix}_qu.id
                 WHERE {$prefix}_qu.id IN ({$insql})";

        if ($contextid) {
            $sql .= " AND {$prefix}_qu.contextid = :{$prefix}_contextid";
            $params["{$prefix}_contextid"] = $contextid;
        }

        $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.
     */
    public static function export_user_data(approved_contextlist $contextlist) {
        global $CFG, $DB, $SITE;
        if (empty($contextlist)) {
            return;
        }

        // Use the Moodle XML Data format.
        // It is the only lossless format that we support.
        $format = "xml";
        require_once($CFG->dirroot . "/question/format/{$format}/format.php");

        // THe export system needs questions in a particular format.
        // The easiest way to fetch these is with get_questions_category() which takes the details of a question
        // category.
        // We fetch the root question category for each context and the get_questions_category function recurses to
        // After fetching them, we filter out any not created or modified by the requestor.
        $user = $contextlist->get_user();
        $userid = $user->id;

        list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
        $categories = $DB->get_records_select('question_categories', "contextid {$contextsql} AND parent = 0", $contextparams);

        $classname = "qformat_{$format}";
        foreach ($categories as $category) {
            $context = \context::instance_by_id($category->contextid);

            $questions = get_questions_category($category, true);
            $questions = array_filter($questions, function($question) use ($userid) {
                return ($question->createdby == $userid) || ($question->modifiedby == $userid);
            }, ARRAY_FILTER_USE_BOTH);

            if (empty($questions)) {
                continue;
            }

            $qformat = new $classname();
            $qformat->setQuestions($questions);

            $qformat->setContexts([$context]);
            $qformat->setContexttofile(true);

            // We do not know which course this belongs to, and it's not actually used except in error, so use Site.
            $qformat->setCourse($SITE);
            $content = '';
            if ($qformat->exportpreprocess()) {
                $content = $qformat->exportprocess(false);
            }

            $subcontext = [
                get_string('questionbank', 'core_question'),
            ];
            writer::with_context($context)->export_custom_file($subcontext, 'questions.xml', $content);
        }
    }

    /**
     * Delete all data for all users in the specified context.
     *
     * @param \context $context The specific context to delete data for.
     * @throws \dml_exception
     */
    public static function delete_data_for_all_users_in_context(\context $context) {
        global $DB;

        // Questions are considered to be 'owned' by the institution, even if they were originally written by a specific
        // user. They are still exported in the list of a users data, but they are not removed.
        // The userid is instead anonymised.

        $sql = 'SELECT q.*
                  FROM {question} q
                  JOIN {question_versions} qv ON qv.questionid = q.id
                  JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
                  JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
                 WHERE qc.contextid = ?';

        $questions = $DB->get_records_sql($sql, [$context->id]);
        foreach ($questions as $question) {
            $question->createdby = 0;
            $question->modifiedby = 0;
            $DB->update_record('question', $question);
        }
    }

    /**
     * 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) {
        global $DB;

        // Questions are considered to be 'owned' by the institution, even if they were originally written by a specific
        // user. They are still exported in the list of a users data, but they are not removed.
        // The userid is instead anonymised.

        list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
        $contextparams['createdby'] = $contextlist->get_user()->id;
        $questiondata = $DB->get_records_sql(
            "SELECT q.*
               FROM {question} q
               JOIN {question_versions} qv ON qv.questionid = q.id
               JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
               JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
              WHERE qc.contextid {$contextsql}
                    AND q.createdby = :createdby", $contextparams);

        foreach ($questiondata as $question) {
            $question->createdby = 0;
            $DB->update_record('question', $question);
        }

        list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
        $contextparams['modifiedby'] = $contextlist->get_user()->id;
        $questiondata = $DB->get_records_sql(
            "SELECT q.*
               FROM {question} q
               JOIN {question_versions} qv ON qv.questionid = q.id
               JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
               JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
              WHERE qc.contextid {$contextsql}
                    AND q.modifiedby = :modifiedby", $contextparams);

        foreach ($questiondata as $question) {
            $question->modifiedby = 0;
            $DB->update_record('question', $question);
        }

    }

    /**
     * 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) {
        global $DB;

        // Questions are considered to be 'owned' by the institution, even if they were originally written by a specific
        // user. They are still exported in the list of a users data, but they are not removed.
        // The userid is instead anonymised.

        $context = $userlist->get_context();
        $userids = $userlist->get_userids();

        list($createdbysql, $createdbyparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
        list($modifiedbysql, $modifiedbyparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);

        $params = ['contextid' => $context->id];

        $questiondata = $DB->get_records_sql(
            "SELECT q.*
               FROM {question} q
               JOIN {question_versions} qv ON qv.questionid = q.id
               JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
               JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
              WHERE qc.contextid = :contextid
                    AND q.createdby {$createdbysql}", $params + $createdbyparams);

        foreach ($questiondata as $question) {
            $question->createdby = 0;
            $DB->update_record('question', $question);
        }

        $questiondata = $DB->get_records_sql(
            "SELECT q.*
               FROM {question} q
               JOIN {question_versions} qv ON qv.questionid = q.id
               JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
               JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
              WHERE qc.contextid = :contextid
                    AND q.modifiedby {$modifiedbysql}", $params + $modifiedbyparams);

        foreach ($questiondata as $question) {
            $question->modifiedby = 0;
            $DB->update_record('question', $question);
        }
    }
}
