Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
namespace core_privacy\local\request;
18
 
19
/**
20
 * The moodle_content_writer is the default Moodle implementation of a content writer.
21
 *
22
 * It exports data to a rich tree structure using Moodle's context system,
23
 * and produces a single zip file with all content.
24
 *
25
 * Objects of data are stored as JSON.
26
 *
27
 * @package core_privacy
28
 * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
29
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
30
 */
31
class moodle_content_writer implements content_writer {
32
    /**
33
     * Maximum context name char size.
34
     */
35
    const MAX_CONTEXT_NAME_LENGTH = 32;
36
 
37
    /**
38
     * @var string The base path on disk for this instance.
39
     */
40
    protected $path = null;
41
 
42
    /**
43
     * @var \context The current context of the writer.
44
     */
45
    protected $context = null;
46
 
47
    /**
48
     * @var \stored_file[] The list of files to be exported.
49
     */
50
    protected $files = [];
51
 
52
    /**
53
     * @var array The list of plugins that have been checked to see if they are installed.
54
     */
55
    protected $checkedplugins = [];
56
 
57
    /**
58
     * Constructor for the content writer.
59
     *
60
     * Note: The writer factory must be passed.
61
     *
62
     * @param   writer          $writer     The factory.
63
     */
64
    public function __construct(writer $writer) {
65
        $this->path = make_request_directory();
66
    }
67
 
68
    /**
69
     * Set the context for the current item being processed.
70
     *
71
     * @param   \context        $context    The context to use
72
     */
73
    public function set_context(\context $context): content_writer {
74
        $this->context = $context;
75
 
76
        return $this;
77
    }
78
 
79
    /**
80
     * Export the supplied data within the current context, at the supplied subcontext.
81
     *
82
     * @param   array           $subcontext The location within the current context that this data belongs.
83
     * @param   \stdClass       $data       The data to be exported
84
     * @return  content_writer
85
     */
86
    public function export_data(array $subcontext, \stdClass $data): content_writer {
87
        $path = $this->get_path($subcontext, 'data.json');
88
 
89
        $this->write_data($path, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
90
 
91
        return $this;
92
    }
93
 
94
    /**
95
     * Export metadata about the supplied subcontext.
96
     *
97
     * Metadata consists of a key/value pair and a description of the value.
98
     *
99
     * @param   array           $subcontext The location within the current context that this data belongs.
100
     * @param   string          $key        The metadata name.
101
     * @param   string          $value      The metadata value.
102
     * @param   string          $description    The description of the value.
103
     * @return  content_writer
104
     */
105
    public function export_metadata(array $subcontext, string $key, $value, string $description): content_writer {
106
        $path = $this->get_full_path($subcontext, 'metadata.json');
107
 
108
        if (file_exists($path)) {
109
            $data = json_decode(file_get_contents($path));
110
        } else {
111
            $data = (object) [];
112
        }
113
 
114
        $data->$key = (object) [
115
            'value' => $value,
116
            'description' => $description,
117
        ];
118
 
119
        $path = $this->get_path($subcontext, 'metadata.json');
120
        $this->write_data($path, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
121
 
122
        return $this;
123
    }
124
 
125
    /**
126
     * Export a piece of related data.
127
     *
128
     * @param   array           $subcontext The location within the current context that this data belongs.
129
     * @param   string          $name       The name of the file to be exported.
130
     * @param   \stdClass       $data       The related data to export.
131
     * @return  content_writer
132
     */
133
    public function export_related_data(array $subcontext, $name, $data): content_writer {
134
        return $this->export_custom_file($subcontext, "{$name}.json",
135
            json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
136
    }
137
 
138
    /**
139
     * Export a piece of data in a custom format.
140
     *
141
     * @param   array           $subcontext The location within the current context that this data belongs.
142
     * @param   string          $filename   The name of the file to be exported.
143
     * @param   string          $filecontent    The content to be exported.
144
     */
145
    public function export_custom_file(array $subcontext, $filename, $filecontent): content_writer {
146
        $filename = clean_param($filename, PARAM_FILE);
147
        $path = $this->get_path($subcontext, $filename);
148
        $this->write_data($path, $filecontent);
149
 
150
        return $this;
151
    }
152
 
153
    /**
154
     * Prepare a text area by processing pluginfile URLs within it.
155
     *
156
     * @param   array           $subcontext The location within the current context that this data belongs.
157
     * @param   string          $component  The name of the component that the files belong to.
158
     * @param   string          $filearea   The filearea within that component.
159
     * @param   string          $itemid     Which item those files belong to.
160
     * @param   string          $text       The text to be processed
161
     * @return  string                      The processed string
162
     */
163
    public function rewrite_pluginfile_urls(array $subcontext, $component, $filearea, $itemid, $text): string {
164
        if ($text === null || $text === '') {
165
            return '';
166
        }
167
        // Need to take into consideration the subcontext to provide the full path to this file.
168
        $subcontextpath = '';
169
        if (!empty($subcontext)) {
170
            $subcontextpath = DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $subcontext);
171
        }
172
        $path = $this->get_context_path();
173
        $path = implode(DIRECTORY_SEPARATOR, $path) . $subcontextpath;
174
        $returnstring = $path . DIRECTORY_SEPARATOR . $this->get_files_target_url($component, $filearea, $itemid) . '/';
175
        $returnstring = clean_param($returnstring, PARAM_PATH);
176
 
177
        return str_replace('@@PLUGINFILE@@/', $returnstring, $text);
178
    }
179
 
180
    /**
181
     * Export all files within the specified component, filearea, itemid combination.
182
     *
183
     * @param   array           $subcontext The location within the current context that this data belongs.
184
     * @param   string          $component  The name of the component that the files belong to.
185
     * @param   string          $filearea   The filearea within that component.
186
     * @param   string          $itemid     Which item those files belong to.
187
     */
188
    public function export_area_files(array $subcontext, $component, $filearea, $itemid): content_writer {
189
        $fs = get_file_storage();
190
        $files = $fs->get_area_files($this->context->id, $component, $filearea, $itemid);
191
        foreach ($files as $file) {
192
            $this->export_file($subcontext, $file);
193
        }
194
 
195
        return $this;
196
    }
197
 
198
    /**
199
     * Export the specified file in the target location.
200
     *
201
     * @param   array           $subcontext The location within the current context that this data belongs.
202
     * @param   \stored_file    $file       The file to be exported.
203
     */
204
    public function export_file(array $subcontext, \stored_file $file): content_writer {
205
        if (!$file->is_directory()) {
206
            $pathitems = array_merge(
207
                $subcontext,
208
                [$this->get_files_target_path($file->get_component(), $file->get_filearea(), $file->get_itemid())],
209
                [$file->get_filepath()]
210
            );
211
            $path = $this->get_path($pathitems, $file->get_filename());
212
            $fullpath = $this->get_full_path($pathitems, $file->get_filename());
213
            check_dir_exists(dirname($fullpath), true, true);
214
            $this->files[$path] = $file;
215
        }
216
 
217
        return $this;
218
    }
219
 
220
    /**
221
     * Export the specified user preference.
222
     *
223
     * @param   string          $component  The name of the component.
224
     * @param   string          $key        The name of th key to be exported.
225
     * @param   string          $value      The value of the preference
226
     * @param   string          $description    A description of the value
227
     * @return  content_writer
228
     */
229
    public function export_user_preference(string $component, string $key, string $value, string $description): content_writer {
230
        $subcontext = [
231
            get_string('userpreferences'),
232
        ];
233
        $fullpath = $this->get_full_path($subcontext, "{$component}.json");
234
        $path = $this->get_path($subcontext, "{$component}.json");
235
 
236
        if (file_exists($fullpath)) {
237
            $data = json_decode(file_get_contents($fullpath));
238
        } else {
239
            $data = (object) [];
240
        }
241
 
242
        $data->$key = (object) [
243
            'value' => $value,
244
            'description' => $description,
245
        ];
246
        $this->write_data($path, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
247
 
248
        return $this;
249
    }
250
 
251
    /**
252
     * Determine the path for the current context.
253
     *
254
     * @return array The context path.
255
     * @throws \coding_exception
256
     */
257
    protected function get_context_path(): array {
258
        $path = [];
259
        $contexts = array_reverse($this->context->get_parent_contexts(true));
260
        foreach ($contexts as $context) {
261
            $name = $context->get_context_name();
262
            $id = ' _.' . $context->id;
263
            $path[] = shorten_text(clean_param($name, PARAM_FILE),
264
                    self::MAX_CONTEXT_NAME_LENGTH, true, json_decode('"' . '\u2026' . '"')) . $id;
265
        }
266
 
267
        return $path;
268
    }
269
 
270
    /**
271
     * Get the relative file path within the current context, and subcontext, using the specified filename.
272
     *
273
     * @param   string[]        $subcontext The location within the current context to export this data.
274
     * @param   string          $name       The intended filename, including any extensions.
275
     * @return  string                      The fully-qualfiied file path.
276
     */
277
    protected function get_path(array $subcontext, string $name): string {
278
        $subcontext = shorten_filenames($subcontext, MAX_FILENAME_SIZE, true);
279
        $name = shorten_filename($name, MAX_FILENAME_SIZE, true);
280
 
281
        // This weird code is to look for a subcontext that contains a number and append an '_' to the front.
282
        // This is because there seems to be some weird problem with array_merge_recursive used in finalise_content().
283
        $subcontext = array_map(function($data) {
284
            if (stripos($data, DIRECTORY_SEPARATOR) !== false) {
285
                $newpath = explode(DIRECTORY_SEPARATOR, $data);
286
                $newpath = array_map(function($value) {
287
                    if (is_numeric($value)) {
288
                        return '_' . $value;
289
                    }
290
                    return $value;
291
                }, $newpath);
292
                $data = implode(DIRECTORY_SEPARATOR, $newpath);
293
            } else if (is_numeric($data)) {
294
                $data = '_' . $data;
295
            }
296
            // Because clean_param() normalises separators to forward-slashes
297
            // and because there is code DIRECTORY_SEPARATOR dependent after
298
            // this array_map(), we ensure we get the original separator.
299
            // Note that maybe we could leave the clean_param() alone, but
300
            // surely that means that the DIRECTORY_SEPARATOR dependent
301
            // code is not needed at all. So better keep existing behavior
302
            // until this is revisited.
303
            return str_replace('/', DIRECTORY_SEPARATOR, clean_param($data, PARAM_PATH));
304
        }, $subcontext);
305
 
306
        // Combine the context path, and the subcontext data.
307
        $path = array_merge(
308
            $this->get_context_path(),
309
            $subcontext
310
        );
311
 
312
        // Join the directory together with the name.
313
        $filepath = implode(DIRECTORY_SEPARATOR, $path) . DIRECTORY_SEPARATOR . $name;
314
 
315
        // To use backslash, it must be doubled ("\\\\" PHP string).
316
        $separator = str_replace('\\', '\\\\', DIRECTORY_SEPARATOR);
317
        return preg_replace('@(' . $separator . '|/)+@', $separator, $filepath);
318
    }
319
 
320
    /**
321
     * Get the fully-qualified file path within the current context, and subcontext, using the specified filename.
322
     *
323
     * @param   string[]        $subcontext The location within the current context to export this data.
324
     * @param   string          $name       The intended filename, including any extensions.
325
     * @return  string                      The fully-qualfiied file path.
326
     */
327
    protected function get_full_path(array $subcontext, string $name): string {
328
        $path = array_merge(
329
            [$this->path],
330
            [$this->get_path($subcontext, $name)]
331
        );
332
 
333
        // Join the directory together with the name.
334
        $filepath = implode(DIRECTORY_SEPARATOR, $path);
335
 
336
        // To use backslash, it must be doubled ("\\\\" PHP string).
337
        $separator = str_replace('\\', '\\\\', DIRECTORY_SEPARATOR);
338
        return preg_replace('@(' . $separator . '|/)+@', $separator, $filepath);
339
    }
340
 
341
    /**
342
     * Get a path within a subcontext where exported files should be written to.
343
     *
344
     * @param string $component The name of the component that the files belong to.
345
     * @param string $filearea The filearea within that component.
346
     * @param string $itemid Which item those files belong to.
347
     * @return string The path
348
     */
349
    protected function get_files_target_path($component, $filearea, $itemid): string {
350
 
351
        // We do not need to include the component because we organise things by context.
352
        $parts = ['_files', $filearea];
353
 
354
        if (!empty($itemid)) {
355
            $parts[] = $itemid;
356
        }
357
 
358
        return implode(DIRECTORY_SEPARATOR, $parts);
359
    }
360
 
361
    /**
362
     * Get a relative url to the directory of the exported files within a subcontext.
363
     *
364
     * @param string $component The name of the component that the files belong to.
365
     * @param string $filearea The filearea within that component.
366
     * @param string $itemid Which item those files belong to.
367
     * @return string The url
368
     */
369
    protected function get_files_target_url($component, $filearea, $itemid): string {
370
        // We do not need to include the component because we organise things by context.
371
        $parts = ['_files', $filearea];
372
 
373
        if (!empty($itemid)) {
374
            $parts[] = '_' . $itemid;
375
        }
376
 
377
        return implode('/', $parts);
378
    }
379
 
380
    /**
381
     * Write the data to the specified path.
382
     *
383
     * @param   string          $path       The path to export the data at.
384
     * @param   string          $data       The data to be exported.
385
     * @throws  \moodle_exception           If the file cannot be written for some reason.
386
     */
387
    protected function write_data(string $path, string $data) {
388
        $targetpath = $this->path . DIRECTORY_SEPARATOR . $path;
389
        check_dir_exists(dirname($targetpath), true, true);
390
        if (file_put_contents($targetpath, $data) === false) {
391
            throw new \moodle_exception('cannotsavefile', 'error', '', $targetpath);
392
        }
393
        $this->files[$path] = $targetpath;
394
    }
395
 
396
    /**
397
     * Copy a file to the specified path.
398
     *
399
     * @param  array  $path        Current location of the file.
400
     * @param  array  $destination Destination path to copy the file to.
401
     */
402
    protected function copy_data(array $path, array $destination) {
403
        global $CFG;
404
        $filename = array_pop($destination);
405
        $destdirectory = implode(DIRECTORY_SEPARATOR, $destination);
406
        $fulldestination = $this->path . DIRECTORY_SEPARATOR . $destdirectory;
407
        check_dir_exists($fulldestination, true, true);
408
        $fulldestination .= $filename;
409
        $currentpath = $CFG->dirroot . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $path);
410
        copy($currentpath, $fulldestination);
411
        $this->files[$destdirectory . DIRECTORY_SEPARATOR . $filename] = $fulldestination;
412
    }
413
 
414
    /**
415
     * This creates three different bits of data from all of the files that will be
416
     * exported.
417
     * $tree - A multidimensional array of the navigation tree structure.
418
     * $treekey - An array with the short path of the file and element data for
419
     *            html (data_file_{number} or 'No var')
420
     * $allfiles - All *.json files that need to be added as an index to be referenced
421
     *             by the js files to display the user data.
422
     *
423
     * @return array returns a tree, tree key, and a list of all files.
424
     */
425
    protected function prepare_for_export(): Array {
426
        $tree = [];
427
        $treekey = [];
428
        $allfiles = [];
429
        $i = 1;
430
        foreach ($this->files as $shortpath => $fullfile) {
431
 
432
            // Generate directory tree as an associative array.
433
            $items = explode(DIRECTORY_SEPARATOR, $shortpath);
434
            $newitems = $this->condense_array($items);
435
            $tree = array_merge_recursive($tree, $newitems);
436
 
437
            if (is_string($fullfile)) {
438
                $filearray = explode(DIRECTORY_SEPARATOR, $shortpath);
439
                $filename = array_pop($filearray);
440
                $filenamearray = explode('.', $filename);
441
                // Don't process files that are not json files.
442
                if (end($filenamearray) !== 'json') {
443
                    continue;
444
                }
445
                // Chop the last two characters of the extension. json => js.
446
                $filename = substr($filename, 0, -2);
447
                array_push($filearray, $filename);
448
                $newshortpath = implode(DIRECTORY_SEPARATOR, $filearray);
449
 
450
                $varname = 'data_file_' . $i;
451
                $i++;
452
 
453
                $quicktemp = clean_param($shortpath, PARAM_PATH);
454
                $treekey[$quicktemp] = $varname;
455
                $allfiles[$varname] = clean_param($newshortpath, PARAM_PATH);
456
 
457
                // Need to load up the current json file and add a variable (varname mentioned above) at the start.
458
                // Then save it as a js file.
459
                $content = $this->get_file_content($fullfile);
460
                $jsondecodedcontent = json_decode($content);
461
                $jsonencodedcontent = json_encode($jsondecodedcontent, JSON_PRETTY_PRINT);
462
                $variablecontent = 'var ' . $varname . ' = ' . $jsonencodedcontent;
463
 
464
                $this->write_data($newshortpath, $variablecontent);
465
            } else {
466
                $treekey[clean_param($shortpath, PARAM_PATH)] = 'No var';
467
            }
468
        }
469
        return [$tree, $treekey, $allfiles];
470
    }
471
 
472
    /**
473
     * Add more detail to the tree to help with sorting and display in the renderer.
474
     *
475
     * @param  array  $tree       The file structure currently as a multidimensional array.
476
     * @param  array  $treekey    An array of the current file paths.
477
     * @param  array  $currentkey The current short path of the tree.
478
     * @return array An array of objects that has additional data.
479
     */
480
    protected function make_tree_object(array $tree, array $treekey, array $currentkey = []): Array {
481
        $newtree = [];
482
        // Try to extract the context id and then add the context object.
483
        $addcontext = function($index, $object) {
484
            if (stripos($index, '_.') !== false) {
485
                $namearray = explode('_.', $index);
486
                $contextid = array_pop($namearray);
487
                if (is_numeric($contextid)) {
488
                    $object[$index]->name = implode('_.', $namearray);
489
                    $object[$index]->context = \context::instance_by_id($contextid);
490
                }
491
            } else {
492
                $object[$index]->name = $index;
493
            }
494
        };
495
        // Just add the final data to the tree object.
496
        $addfinalfile = function($directory, $treeleaf, $file) use ($treekey) {
497
            $url = implode(DIRECTORY_SEPARATOR, $directory);
498
            $url = clean_param($url, PARAM_PATH);
499
            $treeleaf->name = $file;
500
            $treeleaf->itemtype = 'item';
501
            $gokey = clean_param($url . '/' . $file, PARAM_PATH);
502
            if (isset($treekey[$gokey]) && $treekey[$gokey] !== 'No var') {
503
                $treeleaf->datavar = $treekey[$gokey];
504
            } else {
505
                $treeleaf->url = new \moodle_url($url . '/' . $file);
506
            }
507
        };
508
 
509
        foreach ($tree as $key => $value) {
510
            $newtree[$key] = new \stdClass();
511
            if (is_array($value)) {
512
                $newtree[$key]->itemtype = 'treeitem';
513
                // The array merge recursive adds a numeric index, and so we only add to the current
514
                // key if it is now numeric.
515
                $currentkey = is_numeric($key) ? $currentkey : array_merge($currentkey, [$key]);
516
 
517
                // Try to extract the context id and then add the context object.
518
                $addcontext($key, $newtree);
519
                $newtree[$key]->children = $this->make_tree_object($value, $treekey, $currentkey);
520
 
521
                if (!is_numeric($key)) {
522
                    // We're heading back down the tree, so remove the last key.
523
                    array_pop($currentkey);
524
                }
525
            } else {
526
                // If the key is not numeric then we want to add a directory and put the file under that.
527
                if (!is_numeric($key)) {
528
                    $newtree[$key]->itemtype = 'treeitem';
529
                    // Try to extract the context id and then add the context object.
530
                    $addcontext($key, $newtree);
531
                     array_push($currentkey, $key);
532
 
533
                    $newtree[$key]->children[$value] = new \stdClass();
534
                    $addfinalfile($currentkey, $newtree[$key]->children[$value], $value);
535
                    array_pop($currentkey);
536
                } else {
537
                    // If the key is just a number then we just want to show the file instead.
538
                    $addfinalfile($currentkey, $newtree[$key], $value);
539
                }
540
            }
541
        }
542
        return $newtree;
543
    }
544
 
545
    /**
546
     * Sorts the tree list into an order that makes more sense.
547
     * Order is:
548
     * 1 - Items with a context first, the lower the number the higher up the tree.
549
     * 2 - Items that are directories.
550
     * 3 - Items that are log directories.
551
     * 4 - Links to a page.
552
     *
553
     * @param  array $tree The tree structure to order.
554
     */
555
    protected function sort_my_list(array &$tree) {
556
        uasort($tree, function($a, $b) {
557
            if (isset($a->context) && isset($b->context)) {
558
                return $a->context->contextlevel <=> $b->context->contextlevel;
559
            }
560
            if (isset($a->context) && !isset($b->context)) {
561
                return -1;
562
            }
563
            if (isset($b->context) && !isset($a->context)) {
564
                return 1;
565
            }
566
            if ($a->itemtype == 'treeitem' && $b->itemtype == 'treeitem') {
567
                // Ugh need to check that this plugin has not been uninstalled.
568
                if ($this->check_plugin_is_installed('tool_log')) {
569
                    if (trim($a->name) == get_string('privacy:path:logs', 'tool_log')) {
570
                        return 1;
571
                    } else if (trim($b->name) == get_string('privacy:path:logs', 'tool_log')) {
572
                        return -1;
573
                    }
574
                    return 0;
575
                }
576
            }
577
            if ($a->itemtype == 'treeitem' && $b->itemtype == 'item') {
578
                return -1;
579
            }
580
            if ($b->itemtype == 'treeitem' && $a->itemtype == 'item') {
581
                return 1;
582
            }
583
            return 0;
584
        });
585
        foreach ($tree as $treeobject) {
586
            if (isset($treeobject->children)) {
587
                $this->sort_my_list($treeobject->children);
588
            }
589
        }
590
    }
591
 
592
    /**
593
     * Check to see if a specified plugin is installed.
594
     *
595
     * @param  string $component The component name e.g. tool_log
596
     * @return bool Whether this component is installed.
597
     */
598
    protected function check_plugin_is_installed(string $component): Bool {
599
        if (!isset($this->checkedplugins[$component])) {
600
            $pluginmanager = \core_plugin_manager::instance();
601
            $plugin = $pluginmanager->get_plugin_info($component);
602
            $this->checkedplugins[$component] = !is_null($plugin);
603
        }
604
        return $this->checkedplugins[$component];
605
    }
606
 
607
    /**
608
     * Writes the appropriate files for creating an HTML index page for human navigation of the user data export.
609
     */
610
    protected function write_html_data() {
611
        global $PAGE, $SITE, $USER, $CFG;
612
 
613
        // Do this first before adding more files to $this->files.
614
        list($tree, $treekey, $allfiles) = $this->prepare_for_export();
615
        // Add more detail to the tree such as contexts.
616
        $richtree = $this->make_tree_object($tree, $treekey);
617
        // Now that we have more detail we can use that to sort it.
618
        $this->sort_my_list($richtree);
619
 
620
        // Copy over the JavaScript required to display the html page.
621
        $jspath = ['privacy', 'export_files', 'general.js'];
622
        $targetpath = ['js', 'general.js'];
623
        $this->copy_data($jspath, $targetpath);
624
 
625
        $jquery = ['lib', 'jquery', 'jquery-3.7.1.min.js'];
626
        $jquerydestination = ['js', 'jquery-3.7.1.min.js'];
627
        $this->copy_data($jquery, $jquerydestination);
628
 
629
        $requirecurrentpath = ['lib', 'requirejs', 'require.min.js'];
630
        $destination = ['js', 'require.min.js'];
631
        $this->copy_data($requirecurrentpath, $destination);
632
 
633
        $treepath = ['lib', 'amd', 'build', 'tree.min.js'];
634
        $destination = ['js', 'tree.min.js'];
635
        $this->copy_data($treepath, $destination);
636
 
637
        // Icons to be used.
638
        $expandediconpath = ['pix', 't', 'expanded.svg'];
639
        $this->copy_data($expandediconpath, ['pix', 'expanded.svg']);
640
        $collapsediconpath = ['pix', 't', 'collapsed.svg'];
641
        $this->copy_data($collapsediconpath, ['pix', 'collapsed.svg']);
642
        $naviconpath = ['pix', 'i', 'navigationitem.svg'];
643
        $this->copy_data($naviconpath, ['pix', 'navigationitem.svg']);
644
        $moodleimgpath = ['pix', 'moodlelogo.svg'];
645
        $this->copy_data($moodleimgpath, ['pix', 'moodlelogo.svg']);
646
 
647
        // Additional required css.
648
        $csspath = ['theme', 'boost', 'style', 'moodle.css'];
649
        $destination = ['moodle.css'];
650
        $this->copy_data($csspath, $destination);
651
 
652
        $csspath = ['privacy', 'export_files', 'general.css'];
653
        $destination = ['general.css'];
654
        $this->copy_data($csspath, $destination);
655
 
656
        // Create an index file that lists all, to be newly created, js files.
657
        $encoded = json_encode($allfiles,  JSON_PRETTY_PRINT);
658
        $encoded = 'var user_data_index = ' . $encoded;
659
 
660
        $path = 'js' . DIRECTORY_SEPARATOR . 'data_index.js';
661
        $this->write_data($path, $encoded);
662
 
663
        $output = $PAGE->get_renderer('core_privacy');
664
        $navigationpage = new \core_privacy\output\exported_navigation_page(current($richtree));
665
        $navigationhtml = $output->render_navigation($navigationpage);
666
 
667
        $systemname = format_string($SITE->fullname, true, ['context' => \context_system::instance()]);
668
        $fullusername = fullname($USER);
669
        $siteurl = $CFG->wwwroot;
670
 
671
        // Create custom index.html file.
672
        $rtl = right_to_left();
673
        $htmlpage = new \core_privacy\output\exported_html_page($navigationhtml, $systemname, $fullusername, $rtl, $siteurl);
674
        $outputpage = $output->render_html_page($htmlpage);
675
        $this->write_data('index.html', $outputpage);
676
    }
677
 
678
    /**
679
     * Perform any required finalisation steps and return the location of the finalised export.
680
     *
681
     * @return  string
682
     */
683
    public function finalise_content(): string {
684
        $this->write_html_data();
685
 
686
        $exportfile = make_request_directory() . '/export.zip';
687
 
688
        $fp = get_file_packer();
689
        $fp->archive_to_pathname($this->files, $exportfile);
690
 
691
        // Reset the writer to prevent any further writes.
692
        writer::reset();
693
 
694
        return $exportfile;
695
    }
696
 
697
    /**
698
     * Creates a multidimensional array out of array elements.
699
     *
700
     * @param  array  $array Array which items are to be condensed into a multidimensional array.
701
     * @return array The multidimensional array.
702
     */
703
    protected function condense_array(array $array): Array {
704
        if (count($array) === 2) {
705
            return [$array[0] => $array[1]];
706
        }
707
        if (isset($array[0])) {
708
            return [$array[0] => $this->condense_array(array_slice($array, 1))];
709
        }
710
        return [];
711
    }
712
 
713
    /**
714
     * Get the contents of a file.
715
     *
716
     * @param  string $filepath The file path.
717
     * @return string contents of the file.
718
     * @throws \moodle_exception If the file cannot be opened.
719
     */
720
    protected function get_file_content(string $filepath): String {
721
        $content = file_get_contents($filepath);
722
        if ($content === false) {
723
            throw new \moodle_exception('cannotopenfile', 'error', '', $filepath);
724
        }
725
        return $content;
726
    }
727
}