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]

   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   * Unit Tests for the Moodle Content Writer.
  19   *
  20   * @package     core_privacy
  21   * @category    test
  22   * @copyright   2018 Andrew Nicols <andrew@nicols.co.uk>
  23   * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  defined('MOODLE_INTERNAL') || die();
  27  
  28  global $CFG;
  29  
  30  use \core_privacy\local\request\writer;
  31  use \core_privacy\local\request\moodle_content_writer;
  32  
  33  /**
  34   * Tests for the \core_privacy API's moodle_content_writer functionality.
  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   * @coversDefaultClass \core_privacy\local\request\moodle_content_writer
  39   */
  40  class moodle_content_writer_test extends advanced_testcase {
  41  
  42      /**
  43       * Test that exported data is saved correctly within the system context.
  44       *
  45       * @dataProvider export_data_provider
  46       * @param   \stdClass  $data Data
  47       * @covers ::export_data
  48       */
  49      public function test_export_data($data) {
  50          $context = \context_system::instance();
  51          $subcontext = [];
  52  
  53          $writer = $this->get_writer_instance()
  54              ->set_context($context)
  55              ->export_data($subcontext, $data);
  56  
  57          $fileroot = $this->fetch_exported_content($writer);
  58  
  59          $contextpath = $this->get_context_path($context, $subcontext, 'data.json');
  60          $this->assertTrue($fileroot->hasChild($contextpath));
  61  
  62          $json = $fileroot->getChild($contextpath)->getContent();
  63          $expanded = json_decode($json);
  64          $this->assertEquals($data, $expanded);
  65      }
  66  
  67      /**
  68       * Test that exported data is saved correctly for context/subcontext.
  69       *
  70       * @dataProvider export_data_provider
  71       * @param   \stdClass  $data Data
  72       * @covers ::export_data
  73       */
  74      public function test_export_data_different_context($data) {
  75          $context = \context_user::instance(\core_user::get_user_by_username('admin')->id);
  76          $subcontext = ['sub', 'context'];
  77  
  78          $writer = $this->get_writer_instance()
  79              ->set_context($context)
  80              ->export_data($subcontext, $data);
  81  
  82          $fileroot = $this->fetch_exported_content($writer);
  83  
  84          $contextpath = $this->get_context_path($context, $subcontext, 'data.json');
  85          $this->assertTrue($fileroot->hasChild($contextpath));
  86  
  87          $json = $fileroot->getChild($contextpath)->getContent();
  88          $expanded = json_decode($json);
  89          $this->assertEquals($data, $expanded);
  90      }
  91  
  92      /**
  93       * Test that exported is saved within the correct directory locations.
  94       *
  95       * @covers ::export_data
  96       */
  97      public function test_export_data_writes_to_multiple_context() {
  98          $subcontext = ['sub', 'context'];
  99  
 100          $systemcontext = \context_system::instance();
 101          $systemdata = (object) [
 102              'belongsto' => 'system',
 103          ];
 104          $usercontext = \context_user::instance(\core_user::get_user_by_username('admin')->id);
 105          $userdata = (object) [
 106              'belongsto' => 'user',
 107          ];
 108  
 109          $writer = $this->get_writer_instance();
 110  
 111          $writer
 112              ->set_context($systemcontext)
 113              ->export_data($subcontext, $systemdata);
 114  
 115          $writer
 116              ->set_context($usercontext)
 117              ->export_data($subcontext, $userdata);
 118  
 119          $fileroot = $this->fetch_exported_content($writer);
 120  
 121          $contextpath = $this->get_context_path($systemcontext, $subcontext, 'data.json');
 122          $this->assertTrue($fileroot->hasChild($contextpath));
 123  
 124          $json = $fileroot->getChild($contextpath)->getContent();
 125          $expanded = json_decode($json);
 126          $this->assertEquals($systemdata, $expanded);
 127  
 128          $contextpath = $this->get_context_path($usercontext, $subcontext, 'data.json');
 129          $this->assertTrue($fileroot->hasChild($contextpath));
 130  
 131          $json = $fileroot->getChild($contextpath)->getContent();
 132          $expanded = json_decode($json);
 133          $this->assertEquals($userdata, $expanded);
 134      }
 135  
 136      /**
 137       * Test that multiple writes to the same location cause the latest version to be written.
 138       *
 139       * @covers ::export_data
 140       */
 141      public function test_export_data_multiple_writes_same_context() {
 142          $subcontext = ['sub', 'context'];
 143  
 144          $systemcontext = \context_system::instance();
 145          $originaldata = (object) [
 146              'belongsto' => 'system',
 147          ];
 148  
 149          $newdata = (object) [
 150              'abc' => 'def',
 151          ];
 152  
 153          $writer = $this->get_writer_instance();
 154  
 155          $writer
 156              ->set_context($systemcontext)
 157              ->export_data($subcontext, $originaldata);
 158  
 159          $writer
 160              ->set_context($systemcontext)
 161              ->export_data($subcontext, $newdata);
 162  
 163          $fileroot = $this->fetch_exported_content($writer);
 164  
 165          $contextpath = $this->get_context_path($systemcontext, $subcontext, 'data.json');
 166          $this->assertTrue($fileroot->hasChild($contextpath));
 167  
 168          $json = $fileroot->getChild($contextpath)->getContent();
 169          $expanded = json_decode($json);
 170          $this->assertEquals($newdata, $expanded);
 171      }
 172  
 173      /**
 174       * Data provider for exporting user data.
 175       */
 176      public function export_data_provider() {
 177          return [
 178              'basic' => [
 179                  (object) [
 180                      'example' => (object) [
 181                          'key' => 'value',
 182                      ],
 183                  ],
 184              ],
 185          ];
 186      }
 187  
 188      /**
 189       * Test that metadata can be set.
 190       *
 191       * @dataProvider export_metadata_provider
 192       * @param   string  $key Key
 193       * @param   string  $value Value
 194       * @param   string  $description Description
 195       * @covers ::export_metadata
 196       */
 197      public function test_export_metadata($key, $value, $description) {
 198          $context = \context_system::instance();
 199          $subcontext = ['a', 'b', 'c'];
 200  
 201          $writer = $this->get_writer_instance()
 202              ->set_context($context)
 203              ->export_metadata($subcontext, $key, $value, $description);
 204  
 205          $fileroot = $this->fetch_exported_content($writer);
 206  
 207          $contextpath = $this->get_context_path($context, $subcontext, 'metadata.json');
 208          $this->assertTrue($fileroot->hasChild($contextpath));
 209  
 210          $json = $fileroot->getChild($contextpath)->getContent();
 211          $expanded = json_decode($json);
 212          $this->assertTrue(isset($expanded->$key));
 213          $this->assertEquals($value, $expanded->$key->value);
 214          $this->assertEquals($description, $expanded->$key->description);
 215      }
 216  
 217      /**
 218       * Test that metadata can be set additively.
 219       *
 220       * @covers ::export_metadata
 221       */
 222      public function test_export_metadata_additive() {
 223          $context = \context_system::instance();
 224          $subcontext = [];
 225  
 226          $writer = $this->get_writer_instance();
 227  
 228          $writer
 229              ->set_context($context)
 230              ->export_metadata($subcontext, 'firstkey', 'firstvalue', 'firstdescription');
 231  
 232          $writer
 233              ->set_context($context)
 234              ->export_metadata($subcontext, 'secondkey', 'secondvalue', 'seconddescription');
 235  
 236          $fileroot = $this->fetch_exported_content($writer);
 237  
 238          $contextpath = $this->get_context_path($context, $subcontext, 'metadata.json');
 239          $this->assertTrue($fileroot->hasChild($contextpath));
 240  
 241          $json = $fileroot->getChild($contextpath)->getContent();
 242          $expanded = json_decode($json);
 243  
 244          $this->assertTrue(isset($expanded->firstkey));
 245          $this->assertEquals('firstvalue', $expanded->firstkey->value);
 246          $this->assertEquals('firstdescription', $expanded->firstkey->description);
 247  
 248          $this->assertTrue(isset($expanded->secondkey));
 249          $this->assertEquals('secondvalue', $expanded->secondkey->value);
 250          $this->assertEquals('seconddescription', $expanded->secondkey->description);
 251      }
 252  
 253      /**
 254       * Test that metadata can be set additively.
 255       *
 256       * @covers ::export_metadata
 257       */
 258      public function test_export_metadata_to_multiple_contexts() {
 259          $systemcontext = \context_system::instance();
 260          $usercontext = \context_user::instance(\core_user::get_user_by_username('admin')->id);
 261          $subcontext = [];
 262  
 263          $writer = $this->get_writer_instance();
 264  
 265          $writer
 266              ->set_context($systemcontext)
 267              ->export_metadata($subcontext, 'firstkey', 'firstvalue', 'firstdescription')
 268              ->export_metadata($subcontext, 'secondkey', 'secondvalue', 'seconddescription');
 269  
 270          $writer
 271              ->set_context($usercontext)
 272              ->export_metadata($subcontext, 'firstkey', 'alternativevalue', 'alternativedescription')
 273              ->export_metadata($subcontext, 'thirdkey', 'thirdvalue', 'thirddescription');
 274  
 275          $fileroot = $this->fetch_exported_content($writer);
 276  
 277          $systemcontextpath = $this->get_context_path($systemcontext, $subcontext, 'metadata.json');
 278          $this->assertTrue($fileroot->hasChild($systemcontextpath));
 279  
 280          $json = $fileroot->getChild($systemcontextpath)->getContent();
 281          $expanded = json_decode($json);
 282  
 283          $this->assertTrue(isset($expanded->firstkey));
 284          $this->assertEquals('firstvalue', $expanded->firstkey->value);
 285          $this->assertEquals('firstdescription', $expanded->firstkey->description);
 286          $this->assertTrue(isset($expanded->secondkey));
 287          $this->assertEquals('secondvalue', $expanded->secondkey->value);
 288          $this->assertEquals('seconddescription', $expanded->secondkey->description);
 289          $this->assertFalse(isset($expanded->thirdkey));
 290  
 291          $usercontextpath = $this->get_context_path($usercontext, $subcontext, 'metadata.json');
 292          $this->assertTrue($fileroot->hasChild($usercontextpath));
 293  
 294          $json = $fileroot->getChild($usercontextpath)->getContent();
 295          $expanded = json_decode($json);
 296  
 297          $this->assertTrue(isset($expanded->firstkey));
 298          $this->assertEquals('alternativevalue', $expanded->firstkey->value);
 299          $this->assertEquals('alternativedescription', $expanded->firstkey->description);
 300          $this->assertFalse(isset($expanded->secondkey));
 301          $this->assertTrue(isset($expanded->thirdkey));
 302          $this->assertEquals('thirdvalue', $expanded->thirdkey->value);
 303          $this->assertEquals('thirddescription', $expanded->thirdkey->description);
 304      }
 305  
 306      /**
 307       * Data provider for exporting user metadata.
 308       *
 309       * return   array
 310       */
 311      public function export_metadata_provider() {
 312          return [
 313              'basic' => [
 314                  'key',
 315                  'value',
 316                  'This is a description',
 317              ],
 318              'valuewithspaces' => [
 319                  'key',
 320                  'value has mixed',
 321                  'This is a description',
 322              ],
 323              'encodedvalue' => [
 324                  'key',
 325                  base64_encode('value has mixed'),
 326                  'This is a description',
 327              ],
 328          ];
 329      }
 330  
 331      /**
 332       * Exporting a single stored_file should cause that file to be output in the files directory.
 333       *
 334       * @covers ::export_area_files
 335       */
 336      public function test_export_area_files() {
 337          $this->resetAfterTest();
 338          $context = \context_system::instance();
 339          $fs = get_file_storage();
 340  
 341          // Add two files to core_privacy::tests::0.
 342          $files = [];
 343          $file = (object) [
 344              'component' => 'core_privacy',
 345              'filearea' => 'tests',
 346              'itemid' => 0,
 347              'path' => '/',
 348              'name' => 'a.txt',
 349              'content' => 'Test file 0',
 350          ];
 351          $files[] = $file;
 352  
 353          $file = (object) [
 354              'component' => 'core_privacy',
 355              'filearea' => 'tests',
 356              'itemid' => 0,
 357              'path' => '/sub/',
 358              'name' => 'b.txt',
 359              'content' => 'Test file 1',
 360          ];
 361          $files[] = $file;
 362  
 363          // One with a different itemid.
 364          $file = (object) [
 365              'component' => 'core_privacy',
 366              'filearea' => 'tests',
 367              'itemid' => 1,
 368              'path' => '/',
 369              'name' => 'c.txt',
 370              'content' => 'Other',
 371          ];
 372          $files[] = $file;
 373  
 374          // One with a different filearea.
 375          $file = (object) [
 376              'component' => 'core_privacy',
 377              'filearea' => 'alternative',
 378              'itemid' => 0,
 379              'path' => '/',
 380              'name' => 'd.txt',
 381              'content' => 'Alternative',
 382          ];
 383          $files[] = $file;
 384  
 385          // One with a different component.
 386          $file = (object) [
 387              'component' => 'core',
 388              'filearea' => 'tests',
 389              'itemid' => 0,
 390              'path' => '/',
 391              'name' => 'e.txt',
 392              'content' => 'Other tests',
 393          ];
 394          $files[] = $file;
 395  
 396          foreach ($files as $file) {
 397              $record = [
 398                  'contextid' => $context->id,
 399                  'component' => $file->component,
 400                  'filearea'  => $file->filearea,
 401                  'itemid'    => $file->itemid,
 402                  'filepath'  => $file->path,
 403                  'filename'  => $file->name,
 404              ];
 405  
 406              $file->namepath = '/' . $file->filearea . '/' . ($file->itemid ?: '') . $file->path . $file->name;
 407              $file->storedfile = $fs->create_file_from_string($record, $file->content);
 408          }
 409  
 410          $writer = $this->get_writer_instance()
 411              ->set_context($context)
 412              ->export_area_files([], 'core_privacy', 'tests', 0);
 413  
 414          $fileroot = $this->fetch_exported_content($writer);
 415  
 416          $firstfiles = array_slice($files, 0, 2);
 417          foreach ($firstfiles as $file) {
 418              $contextpath = $this->get_context_path($context, ['_files'], $file->namepath);
 419              $this->assertTrue($fileroot->hasChild($contextpath));
 420              $this->assertEquals($file->content, $fileroot->getChild($contextpath)->getContent());
 421          }
 422  
 423          $otherfiles = array_slice($files, 2);
 424          foreach ($otherfiles as $file) {
 425              $contextpath = $this->get_context_path($context, ['_files'], $file->namepath);
 426              $this->assertFalse($fileroot->hasChild($contextpath));
 427          }
 428      }
 429  
 430      /**
 431       * Exporting a single stored_file should cause that file to be output in the files directory.
 432       *
 433       * @dataProvider    export_file_provider
 434       * @param   string  $filearea File area
 435       * @param   int     $itemid Item ID
 436       * @param   string  $filepath File path
 437       * @param   string  $filename File name
 438       * @param   string  $content Content
 439       *
 440       * @covers ::export_file
 441       */
 442      public function test_export_file($filearea, $itemid, $filepath, $filename, $content) {
 443          $this->resetAfterTest();
 444          $context = \context_system::instance();
 445          $filenamepath = '/' . $filearea . '/' . ($itemid ? '_' . $itemid : '') . $filepath . $filename;
 446  
 447          $filerecord = array(
 448              'contextid' => $context->id,
 449              'component' => 'core_privacy',
 450              'filearea'  => $filearea,
 451              'itemid'    => $itemid,
 452              'filepath'  => $filepath,
 453              'filename'  => $filename,
 454          );
 455  
 456          $fs = get_file_storage();
 457          $file = $fs->create_file_from_string($filerecord, $content);
 458  
 459          $writer = $this->get_writer_instance()
 460              ->set_context($context)
 461              ->export_file([], $file);
 462  
 463          $fileroot = $this->fetch_exported_content($writer);
 464  
 465          $contextpath = $this->get_context_path($context, ['_files'], $filenamepath);
 466          $this->assertTrue($fileroot->hasChild($contextpath));
 467          $this->assertEquals($content, $fileroot->getChild($contextpath)->getContent());
 468      }
 469  
 470      /**
 471       * Data provider for the test_export_file function.
 472       *
 473       * @return  array
 474       */
 475      public function export_file_provider() {
 476          return [
 477              'basic' => [
 478                  'intro',
 479                  0,
 480                  '/',
 481                  'testfile.txt',
 482                  'An example file content',
 483              ],
 484              'longpath' => [
 485                  'attachments',
 486                  '12',
 487                  '/path/within/a/path/within/a/path/',
 488                  'testfile.txt',
 489                  'An example file content',
 490              ],
 491              'pathwithspaces' => [
 492                  'intro',
 493                  0,
 494                  '/path with/some spaces/',
 495                  'testfile.txt',
 496                  'An example file content',
 497              ],
 498              'filewithspaces' => [
 499                  'submission_attachments',
 500                  1,
 501                  '/path with/some spaces/',
 502                  'test file.txt',
 503                  'An example file content',
 504              ],
 505              'image' => [
 506                  'intro',
 507                  0,
 508                  '/',
 509                  'logo.png',
 510                  file_get_contents(__DIR__ . '/fixtures/logo.png'),
 511              ],
 512              'UTF8' => [
 513                  'submission_content',
 514                  2,
 515                  '/Žluťoučký/',
 516                  'koníček.txt',
 517                  'koníček',
 518              ],
 519              'EUC-JP' => [
 520                  'intro',
 521                  0,
 522                  '/言語設定/',
 523                  '言語設定.txt',
 524                  '言語設定',
 525              ],
 526          ];
 527      }
 528  
 529      /**
 530       * User preferences can be exported against a user.
 531       *
 532       * @dataProvider    export_user_preference_provider
 533       * @param   string      $component  Component
 534       * @param   string      $key Key
 535       * @param   string      $value Value
 536       * @param   string      $desc Description
 537       * @covers ::export_user_preference
 538       */
 539      public function test_export_user_preference_context_user($component, $key, $value, $desc) {
 540          $admin = \core_user::get_user_by_username('admin');
 541  
 542          $writer = $this->get_writer_instance();
 543  
 544          $context = \context_user::instance($admin->id);
 545          $writer = $this->get_writer_instance()
 546              ->set_context($context)
 547              ->export_user_preference($component, $key, $value, $desc);
 548  
 549          $fileroot = $this->fetch_exported_content($writer);
 550  
 551          $contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json");
 552          $this->assertTrue($fileroot->hasChild($contextpath));
 553  
 554          $json = $fileroot->getChild($contextpath)->getContent();
 555          $expanded = json_decode($json);
 556          $this->assertTrue(isset($expanded->$key));
 557          $data = $expanded->$key;
 558          $this->assertEquals($value, $data->value);
 559          $this->assertEquals($desc, $data->description);
 560      }
 561  
 562      /**
 563       * User preferences can be exported against a course category.
 564       *
 565       * @dataProvider    export_user_preference_provider
 566       * @param   string      $component  Component
 567       * @param   string      $key Key
 568       * @param   string      $value Value
 569       * @param   string      $desc Description
 570       * @covers ::export_user_preference
 571       */
 572      public function test_export_user_preference_context_coursecat($component, $key, $value, $desc) {
 573          global $DB;
 574  
 575          $categories = $DB->get_records('course_categories');
 576          $firstcategory = reset($categories);
 577  
 578          $context = \context_coursecat::instance($firstcategory->id);
 579          $writer = $this->get_writer_instance()
 580              ->set_context($context)
 581              ->export_user_preference($component, $key, $value, $desc);
 582  
 583          $fileroot = $this->fetch_exported_content($writer);
 584  
 585          $contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json");
 586          $this->assertTrue($fileroot->hasChild($contextpath));
 587  
 588          $json = $fileroot->getChild($contextpath)->getContent();
 589          $expanded = json_decode($json);
 590          $this->assertTrue(isset($expanded->$key));
 591          $data = $expanded->$key;
 592          $this->assertEquals($value, $data->value);
 593          $this->assertEquals($desc, $data->description);
 594      }
 595  
 596      /**
 597       * User preferences can be exported against a course.
 598       *
 599       * @dataProvider    export_user_preference_provider
 600       * @param   string      $component  Component
 601       * @param   string      $key Key
 602       * @param   string      $value Value
 603       * @param   string      $desc Description
 604       * @covers ::export_user_preference
 605       */
 606      public function test_export_user_preference_context_course($component, $key, $value, $desc) {
 607          global $DB;
 608  
 609          $this->resetAfterTest();
 610  
 611          $course = $this->getDataGenerator()->create_course();
 612  
 613          $context = \context_course::instance($course->id);
 614          $writer = $this->get_writer_instance()
 615              ->set_context($context)
 616              ->export_user_preference($component, $key, $value, $desc);
 617  
 618          $fileroot = $this->fetch_exported_content($writer);
 619  
 620          $contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json");
 621          $this->assertTrue($fileroot->hasChild($contextpath));
 622  
 623          $json = $fileroot->getChild($contextpath)->getContent();
 624          $expanded = json_decode($json);
 625          $this->assertTrue(isset($expanded->$key));
 626          $data = $expanded->$key;
 627          $this->assertEquals($value, $data->value);
 628          $this->assertEquals($desc, $data->description);
 629      }
 630  
 631      /**
 632       * User preferences can be exported against a module context.
 633       *
 634       * @dataProvider    export_user_preference_provider
 635       * @param   string      $component  Component
 636       * @param   string      $key Key
 637       * @param   string      $value Value
 638       * @param   string      $desc Description
 639       * @covers ::export_user_preference
 640       */
 641      public function test_export_user_preference_context_module($component, $key, $value, $desc) {
 642          global $DB;
 643  
 644          $this->resetAfterTest();
 645  
 646          $course = $this->getDataGenerator()->create_course();
 647          $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
 648  
 649          $context = \context_module::instance($forum->cmid);
 650          $writer = $this->get_writer_instance()
 651              ->set_context($context)
 652              ->export_user_preference($component, $key, $value, $desc);
 653  
 654          $fileroot = $this->fetch_exported_content($writer);
 655  
 656          $contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json");
 657          $this->assertTrue($fileroot->hasChild($contextpath));
 658  
 659          $json = $fileroot->getChild($contextpath)->getContent();
 660          $expanded = json_decode($json);
 661          $this->assertTrue(isset($expanded->$key));
 662          $data = $expanded->$key;
 663          $this->assertEquals($value, $data->value);
 664          $this->assertEquals($desc, $data->description);
 665      }
 666  
 667      /**
 668       * User preferences can not be exported against a block context.
 669       *
 670       * @dataProvider    export_user_preference_provider
 671       * @param   string      $component  Component
 672       * @param   string      $key Key
 673       * @param   string      $value Value
 674       * @param   string      $desc Description
 675       * @covers ::export_user_preference
 676       */
 677      public function test_export_user_preference_context_block($component, $key, $value, $desc) {
 678          global $DB;
 679  
 680          $blocks = $DB->get_records('block_instances');
 681          $block = reset($blocks);
 682  
 683          $context = \context_block::instance($block->id);
 684          $writer = $this->get_writer_instance()
 685              ->set_context($context)
 686              ->export_user_preference($component, $key, $value, $desc);
 687  
 688          $fileroot = $this->fetch_exported_content($writer);
 689  
 690          $contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json");
 691          $this->assertTrue($fileroot->hasChild($contextpath));
 692  
 693          $json = $fileroot->getChild($contextpath)->getContent();
 694          $expanded = json_decode($json);
 695          $this->assertTrue(isset($expanded->$key));
 696          $data = $expanded->$key;
 697          $this->assertEquals($value, $data->value);
 698          $this->assertEquals($desc, $data->description);
 699      }
 700  
 701      /**
 702       * Writing user preferences for two different blocks with the same name and
 703       * same parent context should generate two different context paths and export
 704       * files.
 705       *
 706       * @covers ::export_user_preference
 707       */
 708      public function test_export_user_preference_context_block_multiple_instances() {
 709          $this->resetAfterTest();
 710  
 711          $generator = $this->getDataGenerator();
 712          $course = $generator->create_course();
 713          $coursecontext = context_course::instance($course->id);
 714          $block1 = $generator->create_block('online_users', ['parentcontextid' => $coursecontext->id]);
 715          $block2 = $generator->create_block('online_users', ['parentcontextid' => $coursecontext->id]);
 716          $block1context = context_block::instance($block1->id);
 717          $block2context = context_block::instance($block2->id);
 718          $component = 'block';
 719          $desc = 'test preference';
 720          $block1key = 'block1key';
 721          $block1value = 'block1value';
 722          $block2key = 'block2key';
 723          $block2value = 'block2value';
 724          $writer = $this->get_writer_instance();
 725  
 726          // Confirm that we have two different block contexts with the same name
 727          // and the same parent context id.
 728          $this->assertNotEquals($block1context->id, $block2context->id);
 729          $this->assertEquals($block1context->get_context_name(), $block2context->get_context_name());
 730          $this->assertEquals($block1context->get_parent_context()->id, $block2context->get_parent_context()->id);
 731  
 732          $retrieveexport = function($context) use ($writer, $component) {
 733              $fileroot = $this->fetch_exported_content($writer);
 734  
 735              $contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json");
 736              $this->assertTrue($fileroot->hasChild($contextpath));
 737  
 738              $json = $fileroot->getChild($contextpath)->getContent();
 739              return json_decode($json);
 740          };
 741  
 742          $writer->set_context($block1context)
 743              ->export_user_preference($component, $block1key, $block1value, $desc);
 744          $writer->set_context($block2context)
 745              ->export_user_preference($component, $block2key, $block2value, $desc);
 746  
 747          $block1export = $retrieveexport($block1context);
 748          $block2export = $retrieveexport($block2context);
 749  
 750          // Confirm that the exports didn't write to the same file.
 751          $this->assertTrue(isset($block1export->$block1key));
 752          $this->assertTrue(isset($block2export->$block2key));
 753          $this->assertFalse(isset($block1export->$block2key));
 754          $this->assertFalse(isset($block2export->$block1key));
 755          $this->assertEquals($block1value, $block1export->$block1key->value);
 756          $this->assertEquals($block2value, $block2export->$block2key->value);
 757      }
 758  
 759      /**
 760       * User preferences can be exported against the system.
 761       *
 762       * @dataProvider    export_user_preference_provider
 763       * @param   string      $component  Component
 764       * @param   string      $key Key
 765       * @param   string      $value Value
 766       * @param   string      $desc Description
 767       *
 768       * @covers ::export_user_preference
 769       */
 770      public function test_export_user_preference_context_system($component, $key, $value, $desc) {
 771          $context = \context_system::instance();
 772          $writer = $this->get_writer_instance()
 773              ->set_context($context)
 774              ->export_user_preference($component, $key, $value, $desc);
 775  
 776          $fileroot = $this->fetch_exported_content($writer);
 777  
 778          $contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json");
 779          $this->assertTrue($fileroot->hasChild($contextpath));
 780  
 781          $json = $fileroot->getChild($contextpath)->getContent();
 782          $expanded = json_decode($json);
 783          $this->assertTrue(isset($expanded->$key));
 784          $data = $expanded->$key;
 785          $this->assertEquals($value, $data->value);
 786          $this->assertEquals($desc, $data->description);
 787      }
 788  
 789      /**
 790       * User preferences can be exported against the system.
 791       *
 792       * @covers ::export_user_preference
 793       */
 794      public function test_export_multiple_user_preference_context_system() {
 795          $context = \context_system::instance();
 796          $writer = $this->get_writer_instance();
 797          $component = 'core_privacy';
 798  
 799          $writer
 800              ->set_context($context)
 801              ->export_user_preference($component, 'key1', 'val1', 'desc1')
 802              ->export_user_preference($component, 'key2', 'val2', 'desc2');
 803  
 804          $fileroot = $this->fetch_exported_content($writer);
 805  
 806          $contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json");
 807          $this->assertTrue($fileroot->hasChild($contextpath));
 808  
 809          $json = $fileroot->getChild($contextpath)->getContent();
 810          $expanded = json_decode($json);
 811  
 812          $this->assertTrue(isset($expanded->key1));
 813          $data = $expanded->key1;
 814          $this->assertEquals('val1', $data->value);
 815          $this->assertEquals('desc1', $data->description);
 816  
 817          $this->assertTrue(isset($expanded->key2));
 818          $data = $expanded->key2;
 819          $this->assertEquals('val2', $data->value);
 820          $this->assertEquals('desc2', $data->description);
 821      }
 822  
 823      /**
 824       * User preferences can be exported against the system.
 825       *
 826       * @covers ::export_user_preference
 827       */
 828      public function test_export_user_preference_replace() {
 829          $context = \context_system::instance();
 830          $writer = $this->get_writer_instance();
 831          $component = 'core_privacy';
 832          $key = 'key';
 833  
 834          $writer
 835              ->set_context($context)
 836              ->export_user_preference($component, $key, 'val1', 'desc1');
 837  
 838          $writer
 839              ->set_context($context)
 840              ->export_user_preference($component, $key, 'val2', 'desc2');
 841  
 842          $fileroot = $this->fetch_exported_content($writer);
 843  
 844          $contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json");
 845          $this->assertTrue($fileroot->hasChild($contextpath));
 846  
 847          $json = $fileroot->getChild($contextpath)->getContent();
 848          $expanded = json_decode($json);
 849  
 850          $this->assertTrue(isset($expanded->$key));
 851          $data = $expanded->$key;
 852          $this->assertEquals('val2', $data->value);
 853          $this->assertEquals('desc2', $data->description);
 854      }
 855  
 856      /**
 857       * Provider for various user preferences.
 858       *
 859       * @return  array
 860       */
 861      public function export_user_preference_provider() {
 862          return [
 863              'basic' => [
 864                  'core_privacy',
 865                  'onekey',
 866                  'value',
 867                  'description',
 868              ],
 869              'encodedvalue' => [
 870                  'core_privacy',
 871                  'donkey',
 872                  base64_encode('value'),
 873                  'description',
 874              ],
 875              'long description' => [
 876                  'core_privacy',
 877                  'twokey',
 878                  'value',
 879                  'This is a much longer description which actually states what this is used for. Blah blah blah.',
 880              ],
 881          ];
 882      }
 883  
 884      /**
 885       * Test that exported data is human readable.
 886       *
 887       * @dataProvider unescaped_unicode_export_provider
 888       * @param string $text
 889       * @covers ::export_data
 890       */
 891      public function test_export_data_unescaped_unicode($text) {
 892          $context = \context_system::instance();
 893          $subcontext = [];
 894          $data = (object) ['key' => $text];
 895  
 896          $writer = $this->get_writer_instance()
 897                  ->set_context($context)
 898                  ->export_data($subcontext, $data);
 899  
 900          $fileroot = $this->fetch_exported_content($writer);
 901  
 902          $contextpath = $this->get_context_path($context, $subcontext, 'data.json');
 903  
 904          $json = $fileroot->getChild($contextpath)->getContent();
 905          $this->assertMatchesRegularExpression("/$text/", $json);
 906  
 907          $expanded = json_decode($json);
 908          $this->assertEquals($data, $expanded);
 909      }
 910  
 911      /**
 912       * Test that exported metadata is human readable.
 913       *
 914       * @dataProvider unescaped_unicode_export_provider
 915       * @param string $text
 916       * @covers ::export_metadata
 917       */
 918      public function test_export_metadata_unescaped_unicode($text) {
 919          $context = \context_system::instance();
 920          $subcontext = ['a', 'b', 'c'];
 921  
 922          $writer = $this->get_writer_instance()
 923                  ->set_context($context)
 924                  ->export_metadata($subcontext, $text, $text, $text);
 925  
 926          $fileroot = $this->fetch_exported_content($writer);
 927  
 928          $contextpath = $this->get_context_path($context, $subcontext, 'metadata.json');
 929  
 930          $json = $fileroot->getChild($contextpath)->getContent();
 931          $this->assertMatchesRegularExpression("/$text.*$text.*$text/is", $json);
 932  
 933          $expanded = json_decode($json);
 934          $this->assertTrue(isset($expanded->$text));
 935          $this->assertEquals($text, $expanded->$text->value);
 936          $this->assertEquals($text, $expanded->$text->description);
 937      }
 938  
 939      /**
 940       * Test that exported related data is human readable.
 941       *
 942       * @dataProvider unescaped_unicode_export_provider
 943       * @param string $text
 944       * @covers ::export_related_data
 945       */
 946      public function test_export_related_data_unescaped_unicode($text) {
 947          $context = \context_system::instance();
 948          $subcontext = [];
 949          $data = (object) ['key' => $text];
 950  
 951          $writer = $this->get_writer_instance()
 952                  ->set_context($context)
 953                  ->export_related_data($subcontext, 'name', $data);
 954  
 955          $fileroot = $this->fetch_exported_content($writer);
 956  
 957          $contextpath = $this->get_context_path($context, $subcontext, 'name.json');
 958  
 959          $json = $fileroot->getChild($contextpath)->getContent();
 960          $this->assertMatchesRegularExpression("/$text/", $json);
 961  
 962          $expanded = json_decode($json);
 963          $this->assertEquals($data, $expanded);
 964      }
 965  
 966      /**
 967       * Test that exported related data name is properly cleaned
 968       *
 969       * @covers ::export_related_data
 970       */
 971      public function test_export_related_data_clean_name() {
 972          $context = \context_system::instance();
 973          $subcontext = [];
 974          $data = (object) ['foo' => 'bar'];
 975  
 976          $name = 'Bad/chars:>';
 977  
 978          $writer = $this->get_writer_instance()
 979              ->set_context($context)
 980              ->export_related_data($subcontext, $name, $data);
 981  
 982          $nameclean = clean_param($name, PARAM_FILE);
 983  
 984          $contextpath = $this->get_context_path($context, $subcontext, "{$nameclean}.json");
 985          $expectedpath = "System _.{$context->id}/Badchars.json";
 986          $this->assertEquals($expectedpath, $contextpath);
 987  
 988          $fileroot = $this->fetch_exported_content($writer);
 989          $json = $fileroot->getChild($contextpath)->getContent();
 990  
 991          $this->assertEquals($data, json_decode($json));
 992      }
 993  
 994      /**
 995       * Test that exported user preference is human readable.
 996       *
 997       * @dataProvider unescaped_unicode_export_provider
 998       * @param string $text
 999       * @covers ::export_user_preference
1000       */
1001      public function test_export_user_preference_unescaped_unicode($text) {
1002          $context = \context_system::instance();
1003          $component = 'core_privacy';
1004  
1005          $writer = $this->get_writer_instance()
1006                  ->set_context($context)
1007                  ->export_user_preference($component, $text, $text, $text);
1008  
1009          $fileroot = $this->fetch_exported_content($writer);
1010  
1011          $contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json");
1012  
1013          $json = $fileroot->getChild($contextpath)->getContent();
1014          $this->assertMatchesRegularExpression("/$text.*$text.*$text/is", $json);
1015  
1016          $expanded = json_decode($json);
1017          $this->assertTrue(isset($expanded->$text));
1018          $this->assertEquals($text, $expanded->$text->value);
1019          $this->assertEquals($text, $expanded->$text->description);
1020      }
1021  
1022      /**
1023       * Provider for various user preferences.
1024       *
1025       * @return array
1026       */
1027      public function unescaped_unicode_export_provider() {
1028          return [
1029              'Unicode' => ['ةكءيٓ‌پچژکگیٹڈڑہھےâîûğŞAaÇÖáǽ你好!'],
1030          ];
1031      }
1032  
1033      /**
1034       * Test that exported data subcontext is properly cleaned
1035       *
1036       * @covers ::export_data
1037       */
1038      public function test_export_data_clean_subcontext() {
1039          $context = \context_system::instance();
1040          $subcontext = ['Something/weird', 'More/bad:>', 'Bad&chars:>'];
1041          $data = (object) ['foo' => 'bar'];
1042  
1043          $writer = $this->get_writer_instance()
1044              ->set_context($context)
1045              ->export_data($subcontext, $data);
1046  
1047          $contextpath = $this->get_context_path($context, $subcontext, 'data.json');
1048          $expectedpath = "System _.{$context->id}/Something/weird/More/bad/Badchars/data.json";
1049          $this->assertEquals($expectedpath, $contextpath);
1050  
1051          $fileroot = $this->fetch_exported_content($writer);
1052          $json = $fileroot->getChild($contextpath)->getContent();
1053  
1054          $this->assertEquals($data, json_decode($json));
1055      }
1056  
1057      /**
1058       * Test that exported data is shortened when exceeds the limit.
1059       *
1060       * @dataProvider long_filename_provider
1061       * @param string $longtext
1062       * @param string $expected
1063       * @param string $text
1064       *
1065       * @covers ::export_data
1066       */
1067      public function test_export_data_long_filename($longtext, $expected, $text) {
1068          $context = \context_system::instance();
1069          $subcontext = [$longtext];
1070          $data = (object) ['key' => $text];
1071  
1072          $writer = $this->get_writer_instance()
1073                  ->set_context($context)
1074                  ->export_data($subcontext, $data);
1075  
1076          $fileroot = $this->fetch_exported_content($writer);
1077  
1078          $contextpath = $this->get_context_path($context, $subcontext, 'data.json');
1079          $expectedpath = "System _.{$context->id}/{$expected}/data.json";
1080          $this->assertEquals($expectedpath, $contextpath);
1081  
1082          $json = $fileroot->getChild($contextpath)->getContent();
1083          $this->assertMatchesRegularExpression("/$text/", $json);
1084  
1085          $expanded = json_decode($json);
1086          $this->assertEquals($data, $expanded);
1087      }
1088  
1089      /**
1090       * Test that exported related data is shortened when exceeds the limit.
1091       *
1092       * @dataProvider long_filename_provider
1093       * @param string $longtext
1094       * @param string $expected
1095       * @param string $text
1096       *
1097       * @covers ::export_related_data
1098       */
1099      public function test_export_related_data_long_filename($longtext, $expected, $text) {
1100          $context = \context_system::instance();
1101          $subcontext = [$longtext];
1102          $data = (object) ['key' => $text];
1103  
1104          $writer = $this->get_writer_instance()
1105                  ->set_context($context)
1106                  ->export_related_data($subcontext, 'name', $data);
1107  
1108          $fileroot = $this->fetch_exported_content($writer);
1109  
1110          $contextpath = $this->get_context_path($context, $subcontext, 'name.json');
1111          $expectedpath = "System _.{$context->id}/{$expected}/name.json";
1112          $this->assertEquals($expectedpath, $contextpath);
1113  
1114          $json = $fileroot->getChild($contextpath)->getContent();
1115          $this->assertMatchesRegularExpression("/$text/", $json);
1116  
1117          $expanded = json_decode($json);
1118          $this->assertEquals($data, $expanded);
1119      }
1120  
1121      /**
1122       * Test that exported metadata is shortened when exceeds the limit.
1123       *
1124       * @dataProvider long_filename_provider
1125       * @param string $longtext
1126       * @param string $expected
1127       * @param string $text
1128       */
1129      public function test_export_metadata_long_filename($longtext, $expected, $text) {
1130          $context = \context_system::instance();
1131          $subcontext = [$longtext];
1132          $data = (object) ['key' => $text];
1133  
1134          $writer = $this->get_writer_instance()
1135                  ->set_context($context)
1136                  ->export_metadata($subcontext, $text, $text, $text);
1137  
1138          $fileroot = $this->fetch_exported_content($writer);
1139  
1140          $contextpath = $this->get_context_path($context, $subcontext, 'metadata.json');
1141          $expectedpath = "System _.{$context->id}/{$expected}/metadata.json";
1142          $this->assertEquals($expectedpath, $contextpath);
1143  
1144          $json = $fileroot->getChild($contextpath)->getContent();
1145          $this->assertMatchesRegularExpression("/$text.*$text.*$text/is", $json);
1146  
1147          $expanded = json_decode($json);
1148          $this->assertTrue(isset($expanded->$text));
1149          $this->assertEquals($text, $expanded->$text->value);
1150          $this->assertEquals($text, $expanded->$text->description);
1151      }
1152  
1153      /**
1154       * Test that exported user preference is shortened when exceeds the limit.
1155       *
1156       * @dataProvider long_filename_provider
1157       * @param string $longtext
1158       * @param string $expected
1159       * @param string $text
1160       */
1161      public function test_export_user_preference_long_filename($longtext, $expected, $text) {
1162          $this->resetAfterTest();
1163  
1164          if (!array_key_exists('json', core_filetypes::get_types())) {
1165              // Add json as mime type to avoid lose the extension when shortening filenames.
1166              core_filetypes::add_type('json', 'application/json', 'archive', [], '', 'JSON file archive');
1167          }
1168          $context = \context_system::instance();
1169          $expectedpath = "System _.{$context->id}/User preferences/{$expected}.json";
1170  
1171          $component = $longtext;
1172  
1173          $writer = $this->get_writer_instance()
1174                  ->set_context($context)
1175                  ->export_user_preference($component, $text, $text, $text);
1176  
1177          $fileroot = $this->fetch_exported_content($writer);
1178  
1179          $contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json");
1180          $this->assertEquals($expectedpath, $contextpath);
1181  
1182          $json = $fileroot->getChild($contextpath)->getContent();
1183          $this->assertMatchesRegularExpression("/$text.*$text.*$text/is", $json);
1184  
1185          $expanded = json_decode($json);
1186          $this->assertTrue(isset($expanded->$text));
1187          $this->assertEquals($text, $expanded->$text->value);
1188          $this->assertEquals($text, $expanded->$text->description);
1189      }
1190  
1191      /**
1192       * Provider for long filenames.
1193       *
1194       * @return array
1195       */
1196      public function long_filename_provider() {
1197          return [
1198              'More than 100 characters' => [
1199                  'Etiam sit amet dui vel leo blandit viverra. Proin viverra suscipit velit. Aenean efficitur suscipit nibh nec suscipit',
1200                  'Etiam sit amet dui vel leo blandit viverra. Proin viverra suscipit velit. Aenean effici - 22f7a5030d',
1201                  'value',
1202              ],
1203          ];
1204      }
1205  
1206      /**
1207       * Get a fresh content writer.
1208       *
1209       * @return  moodle_content_writer
1210       */
1211      public function get_writer_instance() {
1212          $factory = $this->createMock(writer::class);
1213          return new moodle_content_writer($factory);
1214      }
1215  
1216      /**
1217       * Fetch the exported content for inspection.
1218       *
1219       * @param   moodle_content_writer   $writer
1220       * @return  \org\bovigo\vfs\vfsStreamDirectory
1221       */
1222      protected function fetch_exported_content(moodle_content_writer $writer) {
1223          $export = $writer
1224              ->set_context(\context_system::instance())
1225              ->finalise_content();
1226  
1227          $fileroot = \org\bovigo\vfs\vfsStream::setup('root');
1228  
1229          $target = \org\bovigo\vfs\vfsStream::url('root');
1230          $fp = get_file_packer();
1231          $fp->extract_to_pathname($export, $target);
1232  
1233          return $fileroot;
1234      }
1235  
1236      /**
1237       * Determine the path for the current context.
1238       *
1239       * Note: This is a wrapper around the real function.
1240       *
1241       * @param   \context        $context    The context being written
1242       * @param   array           $subcontext The subcontext path
1243       * @param   string          $name       THe name of the file target
1244       * @return  array                       The context path.
1245       */
1246      protected function get_context_path($context, $subcontext = null, $name = '') {
1247          $rc = new ReflectionClass(moodle_content_writer::class);
1248          $writer = $this->get_writer_instance();
1249          $writer->set_context($context);
1250  
1251          if (null === $subcontext) {
1252              $rcm = $rc->getMethod('get_context_path');
1253              $rcm->setAccessible(true);
1254              $path = $rcm->invoke($writer);
1255          } else {
1256              $rcm = $rc->getMethod('get_path');
1257              $rcm->setAccessible(true);
1258              $path = $rcm->invoke($writer, $subcontext, $name);
1259          }
1260  
1261          // PHPUnit uses mikey179/vfsStream which is a stream wrapper for a virtual file system that uses '/'
1262          // as the directory separator.
1263          $path = str_replace(DIRECTORY_SEPARATOR, '/', $path);
1264  
1265          return $path;
1266      }
1267  
1268      /**
1269       * Test correct rewriting of @@PLUGINFILE@@ in the exported contents.
1270       *
1271       * @dataProvider rewrite_pluginfile_urls_provider
1272       * @param string $filearea The filearea within that component.
1273       * @param int $itemid Which item those files belong to.
1274       * @param string $input Raw text as stored in the database.
1275       * @param string $expectedoutput Expected output of URL rewriting.
1276       * @covers ::rewrite_pluginfile_urls
1277       */
1278      public function test_rewrite_pluginfile_urls($filearea, $itemid, $input, $expectedoutput) {
1279  
1280          $writer = $this->get_writer_instance();
1281          $writer->set_context(\context_system::instance());
1282  
1283          $realoutput = $writer->rewrite_pluginfile_urls([], 'core_test', $filearea, $itemid, $input);
1284  
1285          $this->assertEquals($expectedoutput, $realoutput);
1286      }
1287  
1288      /**
1289       * Provides testable sample data for {@link self::test_rewrite_pluginfile_urls()}.
1290       *
1291       * @return array
1292       */
1293      public function rewrite_pluginfile_urls_provider() {
1294          return [
1295              'nullcontent' => [
1296                  'intro',
1297                  0,
1298                  null,
1299                  '',
1300              ],
1301              'emptycontent' => [
1302                  'intro',
1303                  0,
1304                  '',
1305                  '',
1306              ],
1307              'zeroitemid' => [
1308                  'intro',
1309                  0,
1310                  '<p><img src="@@PLUGINFILE@@/hello.gif" /></p>',
1311                  '<p><img src="System _.1/_files/intro/hello.gif" /></p>',
1312              ],
1313              'nonzeroitemid' => [
1314                  'submission_content',
1315                  34,
1316                  '<p><img src="@@PLUGINFILE@@/first.png" alt="First" /></p>',
1317                  '<p><img src="System _.1/_files/submission_content/_34/first.png" alt="First" /></p>',
1318              ],
1319              'withfilepath' => [
1320                  'post_content',
1321                  9889,
1322                  '<a href="@@PLUGINFILE@@/embedded/docs/muhehe.exe">Click here!</a>',
1323                  '<a href="System _.1/_files/post_content/_9889/embedded/docs/muhehe.exe">Click here!</a>',
1324              ],
1325          ];
1326      }
1327  
1328      public function test_export_html_functions() {
1329          $this->resetAfterTest();
1330  
1331          $data = (object) ['key' => 'value'];
1332  
1333          $context = \context_system::instance();
1334          $subcontext = [];
1335  
1336          $writer = $this->get_writer_instance()
1337              ->set_context($context)
1338              ->export_data($subcontext, (object) $data);
1339  
1340          $writer->set_context($context)->export_data(['paper'], $data);
1341  
1342          $coursecategory = $this->getDataGenerator()->create_category();
1343          $categorycontext = \context_coursecat::instance($coursecategory->id);
1344          $course = $this->getDataGenerator()->create_course();
1345          $misccoursecxt = \context_coursecat::instance($course->category);
1346          $coursecontext = \context_course::instance($course->id);
1347          $cm = $this->getDataGenerator()->create_module('chat', ['course' => $course->id]);
1348          $modulecontext = \context_module::instance($cm->cmid);
1349  
1350          $writer->set_context($modulecontext)->export_data([], $data);
1351          $writer->set_context($coursecontext)->export_data(['grades'], $data);
1352          $writer->set_context($categorycontext)->export_data([], $data);
1353          $writer->set_context($context)->export_data([get_string('privacy:path:logs', 'tool_log'), 'Standard log'], $data);
1354  
1355          // Add a file.
1356          $fs = get_file_storage();
1357          $file = (object) [
1358              'component' => 'core_privacy',
1359              'filearea' => 'tests',
1360              'itemid' => 0,
1361              'path' => '/',
1362              'name' => 'a.txt',
1363              'content' => 'Test file 0',
1364          ];
1365          $record = [
1366              'contextid' => $context->id,
1367              'component' => $file->component,
1368              'filearea'  => $file->filearea,
1369              'itemid'    => $file->itemid,
1370              'filepath'  => $file->path,
1371              'filename'  => $file->name,
1372          ];
1373  
1374          $file->namepath = '/' . $file->filearea . '/' . ($file->itemid ?: '') . $file->path . $file->name;
1375          $file->storedfile = $fs->create_file_from_string($record, $file->content);
1376          $writer->set_context($context)->export_area_files([], 'core_privacy', 'tests', 0);
1377  
1378          list($tree, $treelist, $indexdata) = phpunit_util::call_internal_method($writer, 'prepare_for_export', [],
1379                  '\core_privacy\local\request\moodle_content_writer');
1380  
1381          $expectedtreeoutput = [
1382              'System _.1' => [
1383                  'data.json',
1384                  'paper' => 'data.json',
1385                  'Category Category 1 _.' . $misccoursecxt->id => [
1386                      'Course Test course 1 _.' . $coursecontext->id => [
1387                          'Chat Chat 1 _.' . $modulecontext->id => 'data.json',
1388                          'grades' => 'data.json'
1389                      ]
1390                  ],
1391                  'Category Course category 1 _.' . $categorycontext->id => 'data.json',
1392                  '_files' => [
1393                      'tests' => 'a.txt'
1394                  ],
1395                  'Logs' => [
1396                      'Standard log' => 'data.json'
1397                  ]
1398              ]
1399          ];
1400          $this->assertEquals($expectedtreeoutput, $tree);
1401  
1402          $expectedlistoutput = [
1403              'System _.1/data.json' => 'data_file_1',
1404              'System _.1/paper/data.json' => 'data_file_2',
1405              'System _.1/Category Category 1 _.' . $misccoursecxt->id . '/Course Test course 1 _.' .
1406                      $coursecontext->id . '/Chat Chat 1 _.' . $modulecontext->id . '/data.json'   => 'data_file_3',
1407              'System _.1/Category Category 1 _.' . $misccoursecxt->id . '/Course Test course 1 _.' .
1408                      $coursecontext->id . '/grades/data.json'   => 'data_file_4',
1409              'System _.1/Category Course category 1 _.' . $categorycontext->id . '/data.json' => 'data_file_5',
1410              'System _.1/_files/tests/a.txt' => 'No var',
1411              'System _.1/Logs/Standard log/data.json' => 'data_file_6'
1412          ];
1413          $this->assertEquals($expectedlistoutput, $treelist);
1414  
1415          $expectedindex = [
1416              'data_file_1' => 'System _.1/data.js',
1417              'data_file_2' => 'System _.1/paper/data.js',
1418              'data_file_3' => 'System _.1/Category Category 1 _.' . $misccoursecxt->id . '/Course Test course 1 _.' .
1419                      $coursecontext->id . '/Chat Chat 1 _.' . $modulecontext->id . '/data.js',
1420              'data_file_4' => 'System _.1/Category Category 1 _.' . $misccoursecxt->id . '/Course Test course 1 _.' .
1421                      $coursecontext->id . '/grades/data.js',
1422              'data_file_5' => 'System _.1/Category Course category 1 _.' . $categorycontext->id . '/data.js',
1423              'data_file_6' => 'System _.1/Logs/Standard log/data.js'
1424          ];
1425          $this->assertEquals($expectedindex, $indexdata);
1426  
1427          $richtree = phpunit_util::call_internal_method($writer, 'make_tree_object', [$tree, $treelist],
1428                  '\core_privacy\local\request\moodle_content_writer');
1429  
1430          // This is a big one.
1431          $expectedrichtree = [
1432              'System _.1' => (object) [
1433                  'itemtype' => 'treeitem',
1434                  'name' => 'System ',
1435                  'context' => \context_system::instance(),
1436                  'children' => [
1437                      (object) [
1438                          'name' => 'data.json',
1439                          'itemtype' => 'item',
1440                          'datavar' => 'data_file_1'
1441                      ],
1442                      'paper' => (object) [
1443                          'itemtype' => 'treeitem',
1444                          'name' => 'paper',
1445                          'children' => [
1446                              'data.json' => (object) [
1447                                  'name' => 'data.json',
1448                                  'itemtype' => 'item',
1449                                  'datavar' => 'data_file_2'
1450                              ]
1451                          ]
1452                      ],
1453                      'Category Category 1 _.' . $misccoursecxt->id => (object) [
1454                          'itemtype' => 'treeitem',
1455                          'name' => 'Category Category 1 ',
1456                          'context' => $misccoursecxt,
1457                          'children' => [
1458                              'Course Test course 1 _.' . $coursecontext->id => (object) [
1459                                  'itemtype' => 'treeitem',
1460                                  'name' => 'Course Test course 1 ',
1461                                  'context' => $coursecontext,
1462                                  'children' => [
1463                                      'Chat Chat 1 _.' . $modulecontext->id => (object) [
1464                                          'itemtype' => 'treeitem',
1465                                          'name' => 'Chat Chat 1 ',
1466                                          'context' => $modulecontext,
1467                                          'children' => [
1468                                              'data.json' => (object) [
1469                                                  'name' => 'data.json',
1470                                                  'itemtype' => 'item',
1471                                                  'datavar' => 'data_file_3'
1472                                              ]
1473                                          ]
1474                                      ],
1475                                      'grades' => (object) [
1476                                          'itemtype' => 'treeitem',
1477                                          'name' => 'grades',
1478                                          'children' => [
1479                                              'data.json' => (object) [
1480                                                  'name' => 'data.json',
1481                                                  'itemtype' => 'item',
1482                                                  'datavar' => 'data_file_4'
1483                                              ]
1484                                          ]
1485                                      ]
1486                                  ]
1487                              ]
1488                          ]
1489                      ],
1490                      'Category Course category 1 _.' . $categorycontext->id => (object) [
1491                          'itemtype' => 'treeitem',
1492                          'name' => 'Category Course category 1 ',
1493                          'context' => $categorycontext,
1494                          'children' => [
1495                              'data.json' => (object) [
1496                                  'name' => 'data.json',
1497                                  'itemtype' => 'item',
1498                                  'datavar' => 'data_file_5'
1499                              ]
1500                          ]
1501                      ],
1502                      '_files' => (object) [
1503                          'itemtype' => 'treeitem',
1504                          'name' => '_files',
1505                          'children' => [
1506                              'tests' => (object) [
1507                                  'itemtype' => 'treeitem',
1508                                  'name' => 'tests',
1509                                  'children' => [
1510                                      'a.txt' => (object) [
1511                                          'name' => 'a.txt',
1512                                          'itemtype' => 'item',
1513                                          'url' => new \moodle_url('System _.1/_files/tests/a.txt')
1514                                      ]
1515                                  ]
1516                              ]
1517                          ]
1518                      ],
1519                      'Logs' => (object) [
1520                          'itemtype' => 'treeitem',
1521                          'name' => 'Logs',
1522                          'children' => [
1523                              'Standard log' => (object) [
1524                                  'itemtype' => 'treeitem',
1525                                  'name' => 'Standard log',
1526                                  'children' => [
1527                                      'data.json' => (object) [
1528                                          'name' => 'data.json',
1529                                          'itemtype' => 'item',
1530                                          'datavar' => 'data_file_6'
1531                                      ]
1532                                  ]
1533                              ]
1534                          ]
1535                      ]
1536                  ]
1537              ]
1538          ];
1539          $this->assertEquals($expectedrichtree, $richtree);
1540  
1541          // The phpunit_util::call_internal_method() method doesn't allow for referenced parameters so we have this joyful code
1542          // instead to do the same thing, but with references working obviously.
1543          $funfunction = function($object, $data) {
1544              return $object->sort_my_list($data);
1545          };
1546  
1547          $funfunction = Closure::bind($funfunction, null, $writer);
1548          $funfunction($writer, $richtree);
1549  
1550          // This is a big one.
1551          $expectedsortedtree = [
1552              'System _.1' => (object) [
1553                  'itemtype' => 'treeitem',
1554                  'name' => 'System ',
1555                  'context' => \context_system::instance(),
1556                  'children' => [
1557                      'Category Category 1 _.' . $misccoursecxt->id => (object) [
1558                          'itemtype' => 'treeitem',
1559                          'name' => 'Category Category 1 ',
1560                          'context' => $misccoursecxt,
1561                          'children' => [
1562                              'Course Test course 1 _.' . $coursecontext->id => (object) [
1563                                  'itemtype' => 'treeitem',
1564                                  'name' => 'Course Test course 1 ',
1565                                  'context' => $coursecontext,
1566                                  'children' => [
1567                                      'Chat Chat 1 _.' . $modulecontext->id => (object) [
1568                                          'itemtype' => 'treeitem',
1569                                          'name' => 'Chat Chat 1 ',
1570                                          'context' => $modulecontext,
1571                                          'children' => [
1572                                              'data.json' => (object) [
1573                                                  'name' => 'data.json',
1574                                                  'itemtype' => 'item',
1575                                                  'datavar' => 'data_file_3'
1576                                              ]
1577                                          ]
1578                                      ],
1579                                      'grades' => (object) [
1580                                          'itemtype' => 'treeitem',
1581                                          'name' => 'grades',
1582                                          'children' => [
1583                                              'data.json' => (object) [
1584                                                  'name' => 'data.json',
1585                                                  'itemtype' => 'item',
1586                                                  'datavar' => 'data_file_4'
1587                                              ]
1588                                          ]
1589                                      ]
1590                                  ]
1591                              ]
1592                          ]
1593                      ],
1594                      'Category Course category 1 _.' . $categorycontext->id => (object) [
1595                          'itemtype' => 'treeitem',
1596                          'name' => 'Category Course category 1 ',
1597                          'context' => $categorycontext,
1598                          'children' => [
1599                              'data.json' => (object) [
1600                                  'name' => 'data.json',
1601                                  'itemtype' => 'item',
1602                                  'datavar' => 'data_file_5'
1603                              ]
1604                          ]
1605                      ],
1606                      '_files' => (object) [
1607                          'itemtype' => 'treeitem',
1608                          'name' => '_files',
1609                          'children' => [
1610                              'tests' => (object) [
1611                                  'itemtype' => 'treeitem',
1612                                  'name' => 'tests',
1613                                  'children' => [
1614                                      'a.txt' => (object) [
1615                                          'name' => 'a.txt',
1616                                          'itemtype' => 'item',
1617                                          'url' => new \moodle_url('System _.1/_files/tests/a.txt')
1618                                      ]
1619                                  ]
1620                              ]
1621                          ]
1622                      ],
1623                      'Logs' => (object) [
1624                          'itemtype' => 'treeitem',
1625                          'name' => 'Logs',
1626                          'children' => [
1627                              'Standard log' => (object) [
1628                                  'itemtype' => 'treeitem',
1629                                  'name' => 'Standard log',
1630                                  'children' => [
1631                                      'data.json' => (object) [
1632                                          'name' => 'data.json',
1633                                          'itemtype' => 'item',
1634                                          'datavar' => 'data_file_6'
1635                                      ]
1636                                  ]
1637                              ]
1638                          ]
1639                      ],
1640                      'paper' => (object) [
1641                          'itemtype' => 'treeitem',
1642                          'name' => 'paper',
1643                          'children' => [
1644                              'data.json' => (object) [
1645                                  'name' => 'data.json',
1646                                  'itemtype' => 'item',
1647                                  'datavar' => 'data_file_2'
1648                              ]
1649                          ]
1650                      ],
1651                      (object) [
1652                          'name' => 'data.json',
1653                          'itemtype' => 'item',
1654                          'datavar' => 'data_file_1'
1655                      ]
1656                  ]
1657              ]
1658          ];
1659          $this->assertEquals($expectedsortedtree, $richtree);
1660      }
1661  }