Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401] [Versions 401 and 402] [Versions 401 and 403]

   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.6.1.min.js'];
 626          $jquerydestination = ['js', 'jquery-3.6.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  }