MDL-62285 privacy: use context id when generating context path
[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 ?: '') . $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/", $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/", $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      * Get a fresh content writer.
974      *
975      * @return  moodle_content_writer
976      */
977     public function get_writer_instance() {
978         $factory = $this->createMock(writer::class);
979         return new moodle_content_writer($factory);
980     }
982     /**
983      * Fetch the exported content for inspection.
984      *
985      * @param   moodle_content_writer   $writer
986      * @return  \org\bovigo\vfs\vfsStreamDirectory
987      */
988     protected function fetch_exported_content(moodle_content_writer $writer) {
989         $export = $writer
990             ->set_context(\context_system::instance())
991             ->finalise_content();
993         $fileroot = \org\bovigo\vfs\vfsStream::setup('root');
995         $target = \org\bovigo\vfs\vfsStream::url('root');
996         $fp = get_file_packer();
997         $fp->extract_to_pathname($export, $target);
999         return $fileroot;
1000     }
1002     /**
1003      * Determine the path for the current context.
1004      *
1005      * Note: This is a wrapper around the real function.
1006      *
1007      * @param   \context        $context    The context being written
1008      * @param   array           $subcontext The subcontext path
1009      * @param   string          $name       THe name of the file target
1010      * @return  array                       The context path.
1011      */
1012     protected function get_context_path($context, $subcontext = null, $name = '') {
1013         $rc = new ReflectionClass(moodle_content_writer::class);
1014         $writer = $this->get_writer_instance();
1015         $writer->set_context($context);
1017         if (null === $subcontext) {
1018             $rcm = $rc->getMethod('get_context_path');
1019             $rcm->setAccessible(true);
1020             return $rcm->invoke($writer);
1021         } else {
1022             $rcm = $rc->getMethod('get_path');
1023             $rcm->setAccessible(true);
1024             return $rcm->invoke($writer, $subcontext, $name);
1025         }
1026     }
1028     /**
1029      * Test correct rewriting of @@PLUGINFILE@@ in the exported contents.
1030      *
1031      * @dataProvider rewrite_pluginfile_urls_provider
1032      * @param string $filearea The filearea within that component.
1033      * @param int $itemid Which item those files belong to.
1034      * @param string $input Raw text as stored in the database.
1035      * @param string $expectedoutput Expected output of URL rewriting.
1036      */
1037     public function test_rewrite_pluginfile_urls($filearea, $itemid, $input, $expectedoutput) {
1039         $writer = $this->get_writer_instance();
1040         $writer->set_context(\context_system::instance());
1042         $realoutput = $writer->rewrite_pluginfile_urls([], 'core_test', $filearea, $itemid, $input);
1044         $this->assertEquals($expectedoutput, $realoutput);
1045     }
1047     /**
1048      * Provides testable sample data for {@link self::test_rewrite_pluginfile_urls()}.
1049      *
1050      * @return array
1051      */
1052     public function rewrite_pluginfile_urls_provider() {
1053         return [
1054             'zeroitemid' => [
1055                 'intro',
1056                 0,
1057                 '<p><img src="@@PLUGINFILE@@/hello.gif" /></p>',
1058                 '<p><img src="_files/intro/hello.gif" /></p>',
1059             ],
1060             'nonzeroitemid' => [
1061                 'submission_content',
1062                 34,
1063                 '<p><img src="@@PLUGINFILE@@/first.png" alt="First" /></p>',
1064                 '<p><img src="_files/submission_content/34/first.png" alt="First" /></p>',
1065             ],
1066             'withfilepath' => [
1067                 'post_content',
1068                 9889,
1069                 '<a href="@@PLUGINFILE@@/embedded/docs/muhehe.exe">Click here!</a>',
1070                 '<a href="_files/post_content/9889/embedded/docs/muhehe.exe">Click here!</a>',
1071             ],
1072         ];
1073     }