Proyectos de Subversion Moodle

Rev

Rev 1 | Mostrar el archivo completo | | | Autoría | Ultima modificación | Ver Log |

Rev 1 Rev 1441
Línea 33... Línea 33...
33
 * Note: parentitemid will contain the category->id for questions
33
 * Note: parentitemid will contain the category->id for questions
34
 *
34
 *
35
 * TODO: Complete phpdocs
35
 * TODO: Complete phpdocs
36
 */
36
 */
37
class restore_questions_parser_processor extends grouped_parser_processor {
37
class restore_questions_parser_processor extends grouped_parser_processor {
-
 
38
    /** @var string XML path in the questions.xml backup file to question categories. */
-
 
39
    protected const CATEGORY_PATH = '/question_categories/question_category';
Línea -... Línea 40...
-
 
40
 
-
 
41
    /** @var string XML path in the questions.xml to question elements within question_category (Moodle 4.0+). */
-
 
42
    protected const QUESTION_SUBPATH =
-
 
43
        '/question_bank_entries/question_bank_entry/question_version/question_versions/questions/question';
-
 
44
 
-
 
45
    /** @var string XML path in the questions.xml to question elements within question_category (before Moodle 4.0). */
-
 
46
    protected const LEGACY_QUESTION_SUBPATH = '/questions/question';
-
 
47
 
-
 
48
    /** @var string String for concatenating data into a string for hashing.*/
-
 
49
    protected const HASHDATA_SEPARATOR = '|HASHDATA|';
-
 
50
 
38
 
51
    /** @var string identifies the current restore. */
-
 
52
    protected string $restoreid;
-
 
53
 
39
    protected $restoreid;
54
    /** @var int during the restore, this tracks the last category we saw. Any questions we see will be in here. */
Línea 40... Línea 55...
40
    protected $lastcatid;
55
    protected int $lastcatid;
-
 
56
 
41
 
57
    public function __construct($restoreid) {
42
    public function __construct($restoreid) {
58
        global $CFG;
43
        $this->restoreid = $restoreid;
59
        $this->restoreid = $restoreid;
44
        $this->lastcatid = 0;
60
        $this->lastcatid = 0;
-
 
61
        parent::__construct();
45
        parent::__construct(array());
62
        // Set the paths we are interested on
-
 
63
        $this->add_path(self::CATEGORY_PATH);
-
 
64
        $this->add_path(self::CATEGORY_PATH . self::QUESTION_SUBPATH, true);
-
 
65
        $this->add_path(self::CATEGORY_PATH . self::LEGACY_QUESTION_SUBPATH, true);
-
 
66
 
-
 
67
        // Add all sub-elements, including those from plugins, as grouped paths with the question tag so that
-
 
68
        // we can create a hash of all question data for comparison with questions in the database.
46
        // Set the paths we are interested on
69
        $this->add_path(self::CATEGORY_PATH . self::QUESTION_SUBPATH . '/question_hints');
-
 
70
        $this->add_path(self::CATEGORY_PATH . self::LEGACY_QUESTION_SUBPATH . '/question_hints');
-
 
71
        $this->add_path(self::CATEGORY_PATH . self::QUESTION_SUBPATH . '/question_hints/question_hint');
-
 
72
        $this->add_path(self::CATEGORY_PATH . self::LEGACY_QUESTION_SUBPATH . '/question_hints/question_hint');
-
 
73
 
-
 
74
        $connectionpoint = new restore_path_element('question', self::CATEGORY_PATH . self::QUESTION_SUBPATH);
-
 
75
        foreach (\core\plugin_manager::instance()->get_plugins_of_type('qtype') as $qtype) {
-
 
76
            $restore = $this->get_qtype_restore($qtype->name);
-
 
77
            if (!$restore) {
-
 
78
                continue;
-
 
79
            }
-
 
80
            $structure = $restore->define_plugin_structure($connectionpoint);
-
 
81
            foreach ($structure as $element) {
-
 
82
                $subpath = str_replace(self::CATEGORY_PATH . self::QUESTION_SUBPATH . '/', '', $element->get_path());
-
 
83
                $pathparts = explode('/', $subpath);
-
 
84
                $path = self::CATEGORY_PATH . self::QUESTION_SUBPATH;
-
 
85
                $legacypath = self::CATEGORY_PATH . self::LEGACY_QUESTION_SUBPATH;
-
 
86
                foreach ($pathparts as $part) {
-
 
87
                    $path .= '/' . $part;
-
 
88
                    $legacypath .= '/' . $part;
-
 
89
                    if (!in_array($path, $this->paths)) {
-
 
90
                        $this->add_path($path);
-
 
91
                        $this->add_path($legacypath);
-
 
92
                    }
-
 
93
                }
47
        $this->add_path('/question_categories/question_category');
94
            }
Línea 48... Línea 95...
48
        $this->add_path('/question_categories/question_category/questions/question');
95
        }
49
    }
96
    }
50
 
97
 
51
    protected function dispatch_chunk($data) {
98
    protected function dispatch_chunk($data) {
52
        // Prepare question_category record
99
        // Prepare question_category record
53
        if ($data['path'] == '/question_categories/question_category') {
100
        if ($data['path'] == self::CATEGORY_PATH) {
54
            $info     = (object)$data['tags'];
101
            $info     = (object)$data['tags'];
55
            $itemname = 'question_category';
102
            $itemname = 'question_category';
Línea 56... Línea 103...
56
            $itemid   = $info->id;
103
            $itemid   = $info->id;
57
            $parentitemid = $info->contextid;
104
            $parentitemid = $info->contextid;
-
 
105
            $this->lastcatid = $itemid;
-
 
106
 
58
            $this->lastcatid = $itemid;
107
        // Prepare question record
59
 
108
        } else if ($data['path'] == self::CATEGORY_PATH . self::QUESTION_SUBPATH ||
60
        // Prepare question record
109
                $data['path'] == self::CATEGORY_PATH . self::LEGACY_QUESTION_SUBPATH) {
61
        } else if ($data['path'] == '/question_categories/question_category/questions/question') {
110
            // Remove sub-elements from the question info we're going to save.
-
 
111
            $info = (object) array_filter($data['tags'], fn($tag) => !is_array($tag));
-
 
112
            $itemname = 'question';
-
 
113
            $itemid   = $info->id;
-
 
114
            $parentitemid = $this->lastcatid;
-
 
115
            $restore = $this->get_qtype_restore($data['tags']['qtype']);
-
 
116
            if ($restore) {
-
 
117
                $questiondata = $restore->convert_backup_to_questiondata($data['tags']);
-
 
118
            } else {
Línea 62... Línea 119...
62
            $info = (object)$data['tags'];
119
                $questiondata = restore_qtype_plugin::convert_backup_to_questiondata($data['tags']);
63
            $itemname = 'question';
120
            }
64
            $itemid   = $info->id;
121
            // Store a hash of question fields for comparison with existing questions.
65
            $parentitemid = $this->lastcatid;
122
            $info->questionhash = $this->generate_question_identity_hash($questiondata);
Línea 90... Línea 147...
90
        if ($cdata === '$@NULL@$') {
147
        if ($cdata === '$@NULL@$') {
91
            return null;
148
            return null;
92
        }
149
        }
93
        return $cdata;
150
        return $cdata;
94
    }
151
    }
-
 
152
 
-
 
153
    /**
-
 
154
     * Load and instantiate the restore class for the given question type.
-
 
155
     *
-
 
156
     * If there is no restore class, null is returned.
-
 
157
     *
-
 
158
     * @param string $qtype The question type name (no qtype_ prefix)
-
 
159
     * @return ?restore_qtype_plugin
-
 
160
     */
-
 
161
    protected static function get_qtype_restore(string $qtype): ?restore_qtype_plugin {
-
 
162
        global $CFG;
-
 
163
        $step = new restore_quiz_activity_structure_step('questions', 'question.xml');
-
 
164
        $filepath = "{$CFG->dirroot}/question/type/{$qtype}/backup/moodle2/restore_qtype_{$qtype}_plugin.class.php";
-
 
165
        if (!file_exists($filepath)) {
-
 
166
            return null;
-
 
167
        }
-
 
168
        require_once($filepath);
-
 
169
        $restoreclass = "restore_qtype_{$qtype}_plugin";
-
 
170
        if (!class_exists($restoreclass)) {
-
 
171
            return null;
-
 
172
        }
-
 
173
        return new $restoreclass('qtype', $qtype, $step);
-
 
174
    }
-
 
175
 
-
 
176
    /**
-
 
177
     * Given a data structure containing the data for a question, reduce it to a flat array and return a sha1 hash of the data.
-
 
178
     *
-
 
179
     * @param stdClass $questiondata An array containing all the data for a question, including hints and qtype plugin data.
-
 
180
     * @param ?backup_xml_transformer $transformer If provided, run the backup transformer process on all text fields. This ensures
-
 
181
     *     that values from the database are compared like-for-like with encoded values from the backup.
-
 
182
     * @return string A sha1 hash of all question data, normalised and concatenated together.
-
 
183
     */
-
 
184
    public static function generate_question_identity_hash(
-
 
185
        stdClass $questiondata,
-
 
186
        ?backup_xml_transformer $transformer = null,
-
 
187
    ): string {
-
 
188
        $questiondata = clone($questiondata);
-
 
189
        $restore = self::get_qtype_restore($questiondata->qtype);
-
 
190
        if ($restore) {
-
 
191
            $restore->define_plugin_structure(new restore_path_element('question', self::CATEGORY_PATH . self::QUESTION_SUBPATH));
-
 
192
            // Combine default exclusions with those specified by the plugin.
-
 
193
            $questiondata = $restore->remove_excluded_question_data($questiondata, $restore->get_excluded_identity_hash_fields());
-
 
194
        } else {
-
 
195
            // The qtype has no restore class, use the default reduction method.
-
 
196
            $questiondata = restore_qtype_plugin::remove_excluded_question_data($questiondata);
-
 
197
        }
-
 
198
 
-
 
199
        // Convert questiondata to a flat array of values.
-
 
200
        $hashdata = [];
-
 
201
        // Convert the object to a multi-dimensional array for compatibility with array_walk_recursive.
-
 
202
        $questiondata = json_decode(json_encode($questiondata), true);
-
 
203
        array_walk_recursive($questiondata, function($value) use (&$hashdata) {
-
 
204
            // Normalise data types. Depending on where the data comes from, it may be a mixture of nulls, strings,
-
 
205
            // ints and floats. Convert everything to strings, then all numbers to floats to ensure we are doing
-
 
206
            // like-for-like comparisons without losing accuracy.
-
 
207
            $value = (string) $value;
-
 
208
            if (is_numeric($value)) {
-
 
209
                $value = (float) ($value);
-
 
210
            } else if (str_contains($value, "\r\n")) {
-
 
211
                // Normalise line breaks.
-
 
212
                $value = str_replace("\r\n", "\n", $value);
-
 
213
            }
-
 
214
            $hashdata[] = $value;
-
 
215
        });
-
 
216
 
-
 
217
        sort($hashdata, SORT_STRING);
-
 
218
        $hashstring = implode(self::HASHDATA_SEPARATOR, $hashdata);
-
 
219
        if ($transformer) {
-
 
220
            $hashstring = $transformer->process($hashstring);
-
 
221
            // Need to re-sort the hashdata with the transformed strings.
-
 
222
            $hashdata = explode(self::HASHDATA_SEPARATOR, $hashstring);
-
 
223
            sort($hashdata, SORT_STRING);
-
 
224
            $hashstring = implode(self::HASHDATA_SEPARATOR, $hashdata);
-
 
225
        }
-
 
226
        return sha1($hashstring);
-
 
227
    }
95
}
228
}