See Release Notes
Long Term Support Release
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body