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