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