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
26defined('MOODLE_INTERNAL') || die();
27
28global $CFG;
29
30use \core_privacy\local\request\writer;
31use \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 */
40class 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            'zeroitemid' => [
1296                'intro',
1297                0,
1298                '<p><img src="@@PLUGINFILE@@/hello.gif" /></p>',
1299                '<p><img src="System _.1/_files/intro/hello.gif" /></p>',
1300            ],
1301            'nonzeroitemid' => [
1302                'submission_content',
1303                34,
1304                '<p><img src="@@PLUGINFILE@@/first.png" alt="First" /></p>',
1305                '<p><img src="System _.1/_files/submission_content/_34/first.png" alt="First" /></p>',
1306            ],
1307            'withfilepath' => [
1308                'post_content',
1309                9889,
1310                '<a href="@@PLUGINFILE@@/embedded/docs/muhehe.exe">Click here!</a>',
1311                '<a href="System _.1/_files/post_content/_9889/embedded/docs/muhehe.exe">Click here!</a>',
1312            ],
1313        ];
1314    }
1315
1316    public function test_export_html_functions() {
1317        $this->resetAfterTest();
1318
1319        $data = (object) ['key' => 'value'];
1320
1321        $context = \context_system::instance();
1322        $subcontext = [];
1323
1324        $writer = $this->get_writer_instance()
1325            ->set_context($context)
1326            ->export_data($subcontext, (object) $data);
1327
1328        $writer->set_context($context)->export_data(['paper'], $data);
1329
1330        $coursecategory = $this->getDataGenerator()->create_category();
1331        $categorycontext = \context_coursecat::instance($coursecategory->id);
1332        $course = $this->getDataGenerator()->create_course();
1333        $misccoursecxt = \context_coursecat::instance($course->category);
1334        $coursecontext = \context_course::instance($course->id);
1335        $cm = $this->getDataGenerator()->create_module('chat', ['course' => $course->id]);
1336        $modulecontext = \context_module::instance($cm->cmid);
1337
1338        $writer->set_context($modulecontext)->export_data([], $data);
1339        $writer->set_context($coursecontext)->export_data(['grades'], $data);
1340        $writer->set_context($categorycontext)->export_data([], $data);
1341        $writer->set_context($context)->export_data([get_string('privacy:path:logs', 'tool_log'), 'Standard log'], $data);
1342
1343        // Add a file.
1344        $fs = get_file_storage();
1345        $file = (object) [
1346            'component' => 'core_privacy',
1347            'filearea' => 'tests',
1348            'itemid' => 0,
1349            'path' => '/',
1350            'name' => 'a.txt',
1351            'content' => 'Test file 0',
1352        ];
1353        $record = [
1354            'contextid' => $context->id,
1355            'component' => $file->component,
1356            'filearea'  => $file->filearea,
1357            'itemid'    => $file->itemid,
1358            'filepath'  => $file->path,
1359            'filename'  => $file->name,
1360        ];
1361
1362        $file->namepath = '/' . $file->filearea . '/' . ($file->itemid ?: '') . $file->path . $file->name;
1363        $file->storedfile = $fs->create_file_from_string($record, $file->content);
1364        $writer->set_context($context)->export_area_files([], 'core_privacy', 'tests', 0);
1365
1366        list($tree, $treelist, $indexdata) = phpunit_util::call_internal_method($writer, 'prepare_for_export', [],
1367                '\core_privacy\local\request\moodle_content_writer');
1368
1369        $expectedtreeoutput = [
1370            'System _.1' => [
1371                'data.json',
1372                'paper' => 'data.json',
1373                'Category Miscellaneous _.' . $misccoursecxt->id => [
1374                    'Course Test course 1 _.' . $coursecontext->id => [
1375                        'Chat Chat 1 _.' . $modulecontext->id => 'data.json',
1376                        'grades' => 'data.json'
1377                    ]
1378                ],
1379                'Category Course category 1 _.' . $categorycontext->id => 'data.json',
1380                '_files' => [
1381                    'tests' => 'a.txt'
1382                ],
1383                'Logs' => [
1384                    'Standard log' => 'data.json'
1385                ]
1386            ]
1387        ];
1388        $this->assertEquals($expectedtreeoutput, $tree);
1389
1390        $expectedlistoutput = [
1391            'System _.1/data.json' => 'data_file_1',
1392            'System _.1/paper/data.json' => 'data_file_2',
1393            'System _.1/Category Miscellaneous _.' . $misccoursecxt->id . '/Course Test course 1 _.' .
1394                    $coursecontext->id . '/Chat Chat 1 _.' . $modulecontext->id . '/data.json'   => 'data_file_3',
1395            'System _.1/Category Miscellaneous _.' . $misccoursecxt->id . '/Course Test course 1 _.' .
1396                    $coursecontext->id . '/grades/data.json'   => 'data_file_4',
1397            'System _.1/Category Course category 1 _.' . $categorycontext->id . '/data.json' => 'data_file_5',
1398            'System _.1/_files/tests/a.txt' => 'No var',
1399            'System _.1/Logs/Standard log/data.json' => 'data_file_6'
1400        ];
1401        $this->assertEquals($expectedlistoutput, $treelist);
1402
1403        $expectedindex = [
1404            'data_file_1' => 'System _.1/data.js',
1405            'data_file_2' => 'System _.1/paper/data.js',
1406            'data_file_3' => 'System _.1/Category Miscellaneous _.' . $misccoursecxt->id . '/Course Test course 1 _.' .
1407                    $coursecontext->id . '/Chat Chat 1 _.' . $modulecontext->id . '/data.js',
1408            'data_file_4' => 'System _.1/Category Miscellaneous _.' . $misccoursecxt->id . '/Course Test course 1 _.' .
1409                    $coursecontext->id . '/grades/data.js',
1410            'data_file_5' => 'System _.1/Category Course category 1 _.' . $categorycontext->id . '/data.js',
1411            'data_file_6' => 'System _.1/Logs/Standard log/data.js'
1412        ];
1413        $this->assertEquals($expectedindex, $indexdata);
1414
1415        $richtree = phpunit_util::call_internal_method($writer, 'make_tree_object', [$tree, $treelist],
1416                '\core_privacy\local\request\moodle_content_writer');
1417
1418        // This is a big one.
1419        $expectedrichtree = [
1420            'System _.1' => (object) [
1421                'itemtype' => 'treeitem',
1422                'name' => 'System ',
1423                'context' => \context_system::instance(),
1424                'children' => [
1425                    (object) [
1426                        'name' => 'data.json',
1427                        'itemtype' => 'item',
1428                        'datavar' => 'data_file_1'
1429                    ],
1430                    'paper' => (object) [
1431                        'itemtype' => 'treeitem',
1432                        'name' => 'paper',
1433                        'children' => [
1434                            'data.json' => (object) [
1435                                'name' => 'data.json',
1436                                'itemtype' => 'item',
1437                                'datavar' => 'data_file_2'
1438                            ]
1439                        ]
1440                    ],
1441                    'Category Miscellaneous _.' . $misccoursecxt->id => (object) [
1442                        'itemtype' => 'treeitem',
1443                        'name' => 'Category Miscellaneous ',
1444                        'context' => $misccoursecxt,
1445                        'children' => [
1446                            'Course Test course 1 _.' . $coursecontext->id => (object) [
1447                                'itemtype' => 'treeitem',
1448                                'name' => 'Course Test course 1 ',
1449                                'context' => $coursecontext,
1450                                'children' => [
1451                                    'Chat Chat 1 _.' . $modulecontext->id => (object) [
1452                                        'itemtype' => 'treeitem',
1453                                        'name' => 'Chat Chat 1 ',
1454                                        'context' => $modulecontext,
1455                                        'children' => [
1456                                            'data.json' => (object) [
1457                                                'name' => 'data.json',
1458                                                'itemtype' => 'item',
1459                                                'datavar' => 'data_file_3'
1460                                            ]
1461                                        ]
1462                                    ],
1463                                    'grades' => (object) [
1464                                        'itemtype' => 'treeitem',
1465                                        'name' => 'grades',
1466                                        'children' => [
1467                                            'data.json' => (object) [
1468                                                'name' => 'data.json',
1469                                                'itemtype' => 'item',
1470                                                'datavar' => 'data_file_4'
1471                                            ]
1472                                        ]
1473                                    ]
1474                                ]
1475                            ]
1476                        ]
1477                    ],
1478                    'Category Course category 1 _.' . $categorycontext->id => (object) [
1479                        'itemtype' => 'treeitem',
1480                        'name' => 'Category Course category 1 ',
1481                        'context' => $categorycontext,
1482                        'children' => [
1483                            'data.json' => (object) [
1484                                'name' => 'data.json',
1485                                'itemtype' => 'item',
1486                                'datavar' => 'data_file_5'
1487                            ]
1488                        ]
1489                    ],
1490                    '_files' => (object) [
1491                        'itemtype' => 'treeitem',
1492                        'name' => '_files',
1493                        'children' => [
1494                            'tests' => (object) [
1495                                'itemtype' => 'treeitem',
1496                                'name' => 'tests',
1497                                'children' => [
1498                                    'a.txt' => (object) [
1499                                        'name' => 'a.txt',
1500                                        'itemtype' => 'item',
1501                                        'url' => new \moodle_url('System _.1/_files/tests/a.txt')
1502                                    ]
1503                                ]
1504                            ]
1505                        ]
1506                    ],
1507                    'Logs' => (object) [
1508                        'itemtype' => 'treeitem',
1509                        'name' => 'Logs',
1510                        'children' => [
1511                            'Standard log' => (object) [
1512                                'itemtype' => 'treeitem',
1513                                'name' => 'Standard log',
1514                                'children' => [
1515                                    'data.json' => (object) [
1516                                        'name' => 'data.json',
1517                                        'itemtype' => 'item',
1518                                        'datavar' => 'data_file_6'
1519                                    ]
1520                                ]
1521                            ]
1522                        ]
1523                    ]
1524                ]
1525            ]
1526        ];
1527        $this->assertEquals($expectedrichtree, $richtree);
1528
1529        // The phpunit_util::call_internal_method() method doesn't allow for referenced parameters so we have this joyful code
1530        // instead to do the same thing, but with references working obviously.
1531        $funfunction = function($object, $data) {
1532            return $object->sort_my_list($data);
1533        };
1534
1535        $funfunction = Closure::bind($funfunction, null, $writer);
1536        $funfunction($writer, $richtree);
1537
1538        // This is a big one.
1539        $expectedsortedtree = [
1540            'System _.1' => (object) [
1541                'itemtype' => 'treeitem',
1542                'name' => 'System ',
1543                'context' => \context_system::instance(),
1544                'children' => [
1545                    'Category Miscellaneous _.' . $misccoursecxt->id => (object) [
1546                        'itemtype' => 'treeitem',
1547                        'name' => 'Category Miscellaneous ',
1548                        'context' => $misccoursecxt,
1549                        'children' => [
1550                            'Course Test course 1 _.' . $coursecontext->id => (object) [
1551                                'itemtype' => 'treeitem',
1552                                'name' => 'Course Test course 1 ',
1553                                'context' => $coursecontext,
1554                                'children' => [
1555                                    'Chat Chat 1 _.' . $modulecontext->id => (object) [
1556                                        'itemtype' => 'treeitem',
1557                                        'name' => 'Chat Chat 1 ',
1558                                        'context' => $modulecontext,
1559                                        'children' => [
1560                                            'data.json' => (object) [
1561                                                'name' => 'data.json',
1562                                                'itemtype' => 'item',
1563                                                'datavar' => 'data_file_3'
1564                                            ]
1565                                        ]
1566                                    ],
1567                                    'grades' => (object) [
1568                                        'itemtype' => 'treeitem',
1569                                        'name' => 'grades',
1570                                        'children' => [
1571                                            'data.json' => (object) [
1572                                                'name' => 'data.json',
1573                                                'itemtype' => 'item',
1574                                                'datavar' => 'data_file_4'
1575                                            ]
1576                                        ]
1577                                    ]
1578                                ]
1579                            ]
1580                        ]
1581                    ],
1582                    'Category Course category 1 _.' . $categorycontext->id => (object) [
1583                        'itemtype' => 'treeitem',
1584                        'name' => 'Category Course category 1 ',
1585                        'context' => $categorycontext,
1586                        'children' => [
1587                            'data.json' => (object) [
1588                                'name' => 'data.json',
1589                                'itemtype' => 'item',
1590                                'datavar' => 'data_file_5'
1591                            ]
1592                        ]
1593                    ],
1594                    '_files' => (object) [
1595                        'itemtype' => 'treeitem',
1596                        'name' => '_files',
1597                        'children' => [
1598                            'tests' => (object) [
1599                                'itemtype' => 'treeitem',
1600                                'name' => 'tests',
1601                                'children' => [
1602                                    'a.txt' => (object) [
1603                                        'name' => 'a.txt',
1604                                        'itemtype' => 'item',
1605                                        'url' => new \moodle_url('System _.1/_files/tests/a.txt')
1606                                    ]
1607                                ]
1608                            ]
1609                        ]
1610                    ],
1611                    'Logs' => (object) [
1612                        'itemtype' => 'treeitem',
1613                        'name' => 'Logs',
1614                        'children' => [
1615                            'Standard log' => (object) [
1616                                'itemtype' => 'treeitem',
1617                                'name' => 'Standard log',
1618                                'children' => [
1619                                    'data.json' => (object) [
1620                                        'name' => 'data.json',
1621                                        'itemtype' => 'item',
1622                                        'datavar' => 'data_file_6'
1623                                    ]
1624                                ]
1625                            ]
1626                        ]
1627                    ],
1628                    'paper' => (object) [
1629                        'itemtype' => 'treeitem',
1630                        'name' => 'paper',
1631                        'children' => [
1632                            'data.json' => (object) [
1633                                'name' => 'data.json',
1634                                'itemtype' => 'item',
1635                                'datavar' => 'data_file_2'
1636                            ]
1637                        ]
1638                    ],
1639                    (object) [
1640                        'name' => 'data.json',
1641                        'itemtype' => 'item',
1642                        'datavar' => 'data_file_1'
1643                    ]
1644                ]
1645            ]
1646        ];
1647        $this->assertEquals($expectedsortedtree, $richtree);
1648    }
1649}
1650