2 // This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
18 * Unit Tests for the Moodle Content Writer.
20 * @package core_privacy
22 * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26 defined('MOODLE_INTERNAL') || die();
30 use \core_privacy\local\request\writer;
31 use \core_privacy\local\request\moodle_content_writer;
34 * Tests for the \core_privacy API's moodle_content_writer functionality.
36 * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
37 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
39 class moodle_content_writer_test extends advanced_testcase {
42 * Test that exported data is saved correctly within the system context.
44 * @dataProvider export_data_provider
45 * @param \stdClass $data Data
47 public function test_export_data($data) {
48 $context = \context_system::instance();
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);
66 * Test that exported data is saved correctly for context/subcontext.
68 * @dataProvider export_data_provider
69 * @param \stdClass $data Data
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);
90 * Test that exported is saved within the correct directory locations.
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',
99 $usercontext = \context_user::instance(\core_user::get_user_by_username('admin')->id);
100 $userdata = (object) [
101 'belongsto' => 'user',
104 $writer = $this->get_writer_instance();
107 ->set_context($systemcontext)
108 ->export_data($subcontext, $systemdata);
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);
132 * Test that multiple writes to the same location cause the latest version to be written.
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',
142 $newdata = (object) [
146 $writer = $this->get_writer_instance();
149 ->set_context($systemcontext)
150 ->export_data($subcontext, $originaldata);
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);
167 * Data provider for exporting user data.
169 public function export_data_provider() {
173 'example' => (object) [
182 * Test that metadata can be set.
184 * @dataProvider export_metadata_provider
185 * @param string $key Key
186 * @param string $value Value
187 * @param string $description Description
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);
210 * Test that metadata can be set additively.
212 public function test_export_metadata_additive() {
213 $context = \context_system::instance();
216 $writer = $this->get_writer_instance();
219 ->set_context($context)
220 ->export_metadata($subcontext, 'firstkey', 'firstvalue', 'firstdescription');
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);
244 * Test that metadata can be set additively.
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);
251 $writer = $this->get_writer_instance();
254 ->set_context($systemcontext)
255 ->export_metadata($subcontext, 'firstkey', 'firstvalue', 'firstdescription')
256 ->export_metadata($subcontext, 'secondkey', 'secondvalue', 'seconddescription');
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);
295 * Data provider for exporting user metadata.
299 public function export_metadata_provider() {
304 'This is a description',
306 'valuewithspaces' => [
309 'This is a description',
313 base64_encode('value has mixed'),
314 'This is a description',
320 * Exporting a single stored_file should cause that file to be output in the files directory.
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.
330 'component' => 'core_privacy',
331 'filearea' => 'tests',
335 'content' => 'Test file 0',
340 'component' => 'core_privacy',
341 'filearea' => 'tests',
345 'content' => 'Test file 1',
349 // One with a different itemid.
351 'component' => 'core_privacy',
352 'filearea' => 'tests',
356 'content' => 'Other',
360 // One with a different filearea.
362 'component' => 'core_privacy',
363 'filearea' => 'alternative',
367 'content' => 'Alternative',
371 // One with a different component.
373 'component' => 'core',
374 'filearea' => 'tests',
378 'content' => 'Other tests',
382 foreach ($files as $file) {
384 'contextid' => $context->id,
385 'component' => $file->component,
386 'filearea' => $file->filearea,
387 'itemid' => $file->itemid,
388 'filepath' => $file->path,
389 'filename' => $file->name,
392 $file->namepath = '/' . $file->filearea . '/' . ($file->itemid ?: '') . $file->path . $file->name;
393 $file->storedfile = $fs->create_file_from_string($record, $file->content);
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());
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));
417 * Exporting a single stored_file should cause that file to be output in the files directory.
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
426 public function test_export_file($filearea, $itemid, $filepath, $filename, $content) {
427 $this->resetAfterTest();
428 $context = \context_system::instance();
429 $filenamepath = '/' . $filearea . '/' . ($itemid ? '_' . $itemid : '') . $filepath . $filename;
432 'contextid' => $context->id,
433 'component' => 'core_privacy',
434 'filearea' => $filearea,
436 'filepath' => $filepath,
437 'filename' => $filename,
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());
455 * Data provider for the test_export_file function.
459 public function export_file_provider() {
466 'An example file content',
471 '/path/within/a/path/within/a/path/',
473 'An example file content',
475 'pathwithspaces' => [
478 '/path with/some spaces/',
480 'An example file content',
482 'filewithspaces' => [
483 'submission_attachments',
485 '/path with/some spaces/',
487 'An example file content',
494 file_get_contents(__DIR__ . '/fixtures/logo.png'),
497 'submission_content',
514 * User preferences can be exported against a user.
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
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);
546 * User preferences can be exported against a course category.
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
554 public function test_export_user_preference_context_coursecat($component, $key, $value, $desc) {
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);
579 * User preferences can be exported against a course.
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
587 public function test_export_user_preference_context_course($component, $key, $value, $desc) {
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);
613 * User preferences can be exported against a module context.
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
621 public function test_export_user_preference_context_module($component, $key, $value, $desc) {
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);
648 * User preferences can not be exported against a block context.
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
656 public function test_export_user_preference_context_block($component, $key, $value, $desc) {
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);
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
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);
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);
737 * User preferences can be exported against the system.
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
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);
765 * User preferences can be exported against the system.
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';
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);
797 * User preferences can be exported against the system.
799 public function test_export_user_preference_replace() {
800 $context = \context_system::instance();
801 $writer = $this->get_writer_instance();
802 $component = 'core_privacy';
806 ->set_context($context)
807 ->export_user_preference($component, $key, 'val1', 'desc1');
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);
828 * Provider for various user preferences.
832 public function export_user_preference_provider() {
843 base64_encode('value'),
846 'long description' => [
850 'This is a much longer description which actually states what this is used for. Blah blah blah.',
856 * Test that exported data is human readable.
858 * @dataProvider unescaped_unicode_export_provider
859 * @param string $text
861 public function test_export_data_unescaped_unicode($text) {
862 $context = \context_system::instance();
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);
882 * Test that exported metadata is human readable.
884 * @dataProvider unescaped_unicode_export_provider
885 * @param string $text
887 public function test_export_metadata_unescaped_unicode($text) {
888 $context = \context_system::instance();
889 $subcontext = ['a', 'b', 'c'];
891 $writer = $this->get_writer_instance()
892 ->set_context($context)
893 ->export_metadata($subcontext, $text, $text, $text);
895 $fileroot = $this->fetch_exported_content($writer);
897 $contextpath = $this->get_context_path($context, $subcontext, 'metadata.json');
899 $json = $fileroot->getChild($contextpath)->getContent();
900 $this->assertRegExp("/$text.*$text.*$text/is", $json);
902 $expanded = json_decode($json);
903 $this->assertTrue(isset($expanded->$text));
904 $this->assertEquals($text, $expanded->$text->value);
905 $this->assertEquals($text, $expanded->$text->description);
909 * Test that exported related data is human readable.
911 * @dataProvider unescaped_unicode_export_provider
912 * @param string $text
914 public function test_export_related_data_unescaped_unicode($text) {
915 $context = \context_system::instance();
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);
935 * Test that exported user preference is human readable.
937 * @dataProvider unescaped_unicode_export_provider
938 * @param string $text
940 public function test_export_user_preference_unescaped_unicode($text) {
941 $context = \context_system::instance();
942 $component = 'core_privacy';
944 $writer = $this->get_writer_instance()
945 ->set_context($context)
946 ->export_user_preference($component, $text, $text, $text);
948 $fileroot = $this->fetch_exported_content($writer);
950 $contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json");
952 $json = $fileroot->getChild($contextpath)->getContent();
953 $this->assertRegExp("/$text.*$text.*$text/is", $json);
955 $expanded = json_decode($json);
956 $this->assertTrue(isset($expanded->$text));
957 $this->assertEquals($text, $expanded->$text->value);
958 $this->assertEquals($text, $expanded->$text->description);
962 * Provider for various user preferences.
966 public function unescaped_unicode_export_provider() {
968 'Unicode' => ['ةكءيٓپچژکگیٹڈڑہھےâîûğŞAaÇÖáǽ你好!'],
973 * Test that exported data is shortened when exceeds the limit.
975 * @dataProvider long_filename_provider
976 * @param string $longtext
977 * @param string $expected
978 * @param string $text
980 public function test_export_data_long_filename($longtext, $expected, $text) {
981 $context = \context_system::instance();
982 $subcontext = [$longtext];
983 $data = (object) ['key' => $text];
985 $writer = $this->get_writer_instance()
986 ->set_context($context)
987 ->export_data($subcontext, $data);
989 $fileroot = $this->fetch_exported_content($writer);
991 $contextpath = $this->get_context_path($context, $subcontext, 'data.json');
992 $expectedpath = "System _.{$context->id}/{$expected}/data.json";
993 $this->assertEquals($expectedpath, $contextpath);
995 $json = $fileroot->getChild($contextpath)->getContent();
996 $this->assertRegExp("/$text/", $json);
998 $expanded = json_decode($json);
999 $this->assertEquals($data, $expanded);
1003 * Test that exported related data is shortened when exceeds the limit.
1005 * @dataProvider long_filename_provider
1006 * @param string $longtext
1007 * @param string $expected
1008 * @param string $text
1010 public function test_export_related_data_long_filename($longtext, $expected, $text) {
1011 $context = \context_system::instance();
1012 $subcontext = [$longtext];
1013 $data = (object) ['key' => $text];
1015 $writer = $this->get_writer_instance()
1016 ->set_context($context)
1017 ->export_related_data($subcontext, 'name', $data);
1019 $fileroot = $this->fetch_exported_content($writer);
1021 $contextpath = $this->get_context_path($context, $subcontext, 'name.json');
1022 $expectedpath = "System _.{$context->id}/{$expected}/name.json";
1023 $this->assertEquals($expectedpath, $contextpath);
1025 $json = $fileroot->getChild($contextpath)->getContent();
1026 $this->assertRegExp("/$text/", $json);
1028 $expanded = json_decode($json);
1029 $this->assertEquals($data, $expanded);
1033 * Test that exported metadata is shortened when exceeds the limit.
1035 * @dataProvider long_filename_provider
1036 * @param string $longtext
1037 * @param string $expected
1038 * @param string $text
1040 public function test_export_metadata_long_filename($longtext, $expected, $text) {
1041 $context = \context_system::instance();
1042 $subcontext = [$longtext];
1043 $data = (object) ['key' => $text];
1045 $writer = $this->get_writer_instance()
1046 ->set_context($context)
1047 ->export_metadata($subcontext, $text, $text, $text);
1049 $fileroot = $this->fetch_exported_content($writer);
1051 $contextpath = $this->get_context_path($context, $subcontext, 'metadata.json');
1052 $expectedpath = "System _.{$context->id}/{$expected}/metadata.json";
1053 $this->assertEquals($expectedpath, $contextpath);
1055 $json = $fileroot->getChild($contextpath)->getContent();
1056 $this->assertRegExp("/$text.*$text.*$text/is", $json);
1058 $expanded = json_decode($json);
1059 $this->assertTrue(isset($expanded->$text));
1060 $this->assertEquals($text, $expanded->$text->value);
1061 $this->assertEquals($text, $expanded->$text->description);
1065 * Test that exported user preference is shortened when exceeds the limit.
1067 * @dataProvider long_filename_provider
1068 * @param string $longtext
1069 * @param string $expected
1070 * @param string $text
1072 public function test_export_user_preference_long_filename($longtext, $expected, $text) {
1073 $this->resetAfterTest();
1075 if (!array_key_exists('json', core_filetypes::get_types())) {
1076 // Add json as mime type to avoid lose the extension when shortening filenames.
1077 core_filetypes::add_type('json', 'application/json', 'archive', [], '', 'JSON file archive');
1079 $context = \context_system::instance();
1080 $expectedpath = "System _.{$context->id}/User preferences/{$expected}.json";
1082 $component = $longtext;
1084 $writer = $this->get_writer_instance()
1085 ->set_context($context)
1086 ->export_user_preference($component, $text, $text, $text);
1088 $fileroot = $this->fetch_exported_content($writer);
1090 $contextpath = $this->get_context_path($context, [get_string('userpreferences')], "{$component}.json");
1091 $this->assertEquals($expectedpath, $contextpath);
1093 $json = $fileroot->getChild($contextpath)->getContent();
1094 $this->assertRegExp("/$text.*$text.*$text/is", $json);
1096 $expanded = json_decode($json);
1097 $this->assertTrue(isset($expanded->$text));
1098 $this->assertEquals($text, $expanded->$text->value);
1099 $this->assertEquals($text, $expanded->$text->description);
1103 * Provider for long filenames.
1107 public function long_filename_provider() {
1109 'More than 100 characters' => [
1110 'Etiam sit amet dui vel leo blandit viverra. Proin viverra suscipit velit. Aenean efficitur suscipit nibh nec suscipit',
1111 'Etiam sit amet dui vel leo blandit viverra. Proin viverra suscipit velit. Aenean effici - 22f7a5030d',
1118 * Get a fresh content writer.
1120 * @return moodle_content_writer
1122 public function get_writer_instance() {
1123 $factory = $this->createMock(writer::class);
1124 return new moodle_content_writer($factory);
1128 * Fetch the exported content for inspection.
1130 * @param moodle_content_writer $writer
1131 * @return \org\bovigo\vfs\vfsStreamDirectory
1133 protected function fetch_exported_content(moodle_content_writer $writer) {
1135 ->set_context(\context_system::instance())
1136 ->finalise_content();
1138 $fileroot = \org\bovigo\vfs\vfsStream::setup('root');
1140 $target = \org\bovigo\vfs\vfsStream::url('root');
1141 $fp = get_file_packer();
1142 $fp->extract_to_pathname($export, $target);
1148 * Determine the path for the current context.
1150 * Note: This is a wrapper around the real function.
1152 * @param \context $context The context being written
1153 * @param array $subcontext The subcontext path
1154 * @param string $name THe name of the file target
1155 * @return array The context path.
1157 protected function get_context_path($context, $subcontext = null, $name = '') {
1158 $rc = new ReflectionClass(moodle_content_writer::class);
1159 $writer = $this->get_writer_instance();
1160 $writer->set_context($context);
1162 if (null === $subcontext) {
1163 $rcm = $rc->getMethod('get_context_path');
1164 $rcm->setAccessible(true);
1165 $path = $rcm->invoke($writer);
1167 $rcm = $rc->getMethod('get_path');
1168 $rcm->setAccessible(true);
1169 $path = $rcm->invoke($writer, $subcontext, $name);
1172 // PHPUnit uses mikey179/vfsStream which is a stream wrapper for a virtual file system that uses '/'
1173 // as the directory separator.
1174 $path = str_replace(DIRECTORY_SEPARATOR, '/', $path);
1180 * Test correct rewriting of @@PLUGINFILE@@ in the exported contents.
1182 * @dataProvider rewrite_pluginfile_urls_provider
1183 * @param string $filearea The filearea within that component.
1184 * @param int $itemid Which item those files belong to.
1185 * @param string $input Raw text as stored in the database.
1186 * @param string $expectedoutput Expected output of URL rewriting.
1188 public function test_rewrite_pluginfile_urls($filearea, $itemid, $input, $expectedoutput) {
1190 $writer = $this->get_writer_instance();
1191 $writer->set_context(\context_system::instance());
1193 $realoutput = $writer->rewrite_pluginfile_urls([], 'core_test', $filearea, $itemid, $input);
1195 $this->assertEquals($expectedoutput, $realoutput);
1199 * Provides testable sample data for {@link self::test_rewrite_pluginfile_urls()}.
1203 public function rewrite_pluginfile_urls_provider() {
1208 '<p><img src="@@PLUGINFILE@@/hello.gif" /></p>',
1209 '<p><img src="System _.1/_files/intro/hello.gif" /></p>',
1211 'nonzeroitemid' => [
1212 'submission_content',
1214 '<p><img src="@@PLUGINFILE@@/first.png" alt="First" /></p>',
1215 '<p><img src="System _.1/_files/submission_content/_34/first.png" alt="First" /></p>',
1220 '<a href="@@PLUGINFILE@@/embedded/docs/muhehe.exe">Click here!</a>',
1221 '<a href="System _.1/_files/post_content/_9889/embedded/docs/muhehe.exe">Click here!</a>',
1226 public function test_export_html_functions() {
1227 $this->resetAfterTest();
1229 $data = (object) ['key' => 'value'];
1231 $context = \context_system::instance();
1234 $writer = $this->get_writer_instance()
1235 ->set_context($context)
1236 ->export_data($subcontext, (object) $data);
1238 $writer->set_context($context)->export_data(['paper'], $data);
1240 $coursecategory = $this->getDataGenerator()->create_category();
1241 $categorycontext = \context_coursecat::instance($coursecategory->id);
1242 $course = $this->getDataGenerator()->create_course();
1243 $misccoursecxt = \context_coursecat::instance($course->category);
1244 $coursecontext = \context_course::instance($course->id);
1245 $cm = $this->getDataGenerator()->create_module('chat', ['course' => $course->id]);
1246 $modulecontext = \context_module::instance($cm->cmid);
1248 $writer->set_context($modulecontext)->export_data([], $data);
1249 $writer->set_context($coursecontext)->export_data(['grades'], $data);
1250 $writer->set_context($categorycontext)->export_data([], $data);
1251 $writer->set_context($context)->export_data([get_string('privacy:path:logs', 'tool_log'), 'Standard log'], $data);
1254 $fs = get_file_storage();
1256 'component' => 'core_privacy',
1257 'filearea' => 'tests',
1261 'content' => 'Test file 0',
1264 'contextid' => $context->id,
1265 'component' => $file->component,
1266 'filearea' => $file->filearea,
1267 'itemid' => $file->itemid,
1268 'filepath' => $file->path,
1269 'filename' => $file->name,
1272 $file->namepath = '/' . $file->filearea . '/' . ($file->itemid ?: '') . $file->path . $file->name;
1273 $file->storedfile = $fs->create_file_from_string($record, $file->content);
1274 $writer->set_context($context)->export_area_files([], 'core_privacy', 'tests', 0);
1276 list($tree, $treelist, $indexdata) = phpunit_util::call_internal_method($writer, 'prepare_for_export', [],
1277 '\core_privacy\local\request\moodle_content_writer');
1279 $expectedtreeoutput = [
1282 'paper' => 'data.json',
1283 'Category Miscellaneous _.' . $misccoursecxt->id => [
1284 'Course Test course 1 _.' . $coursecontext->id => [
1285 'Chat Chat 1 _.' . $modulecontext->id => 'data.json',
1286 'grades' => 'data.json'
1289 'Category Course category 1 _.' . $categorycontext->id => 'data.json',
1294 'Standard log' => 'data.json'
1298 $this->assertEquals($expectedtreeoutput, $tree);
1300 $expectedlistoutput = [
1301 'System _.1/data.json' => 'data_file_1',
1302 'System _.1/paper/data.json' => 'data_file_2',
1303 'System _.1/Category Miscellaneous _.' . $misccoursecxt->id . '/Course Test course 1 _.' .
1304 $coursecontext->id . '/Chat Chat 1 _.' . $modulecontext->id . '/data.json' => 'data_file_3',
1305 'System _.1/Category Miscellaneous _.' . $misccoursecxt->id . '/Course Test course 1 _.' .
1306 $coursecontext->id . '/grades/data.json' => 'data_file_4',
1307 'System _.1/Category Course category 1 _.' . $categorycontext->id . '/data.json' => 'data_file_5',
1308 'System _.1/_files/tests/a.txt' => 'No var',
1309 'System _.1/Logs/Standard log/data.json' => 'data_file_6'
1311 $this->assertEquals($expectedlistoutput, $treelist);
1314 'data_file_1' => 'System _.1/data.js',
1315 'data_file_2' => 'System _.1/paper/data.js',
1316 'data_file_3' => 'System _.1/Category Miscellaneous _.' . $misccoursecxt->id . '/Course Test course 1 _.' .
1317 $coursecontext->id . '/Chat Chat 1 _.' . $modulecontext->id . '/data.js',
1318 'data_file_4' => 'System _.1/Category Miscellaneous _.' . $misccoursecxt->id . '/Course Test course 1 _.' .
1319 $coursecontext->id . '/grades/data.js',
1320 'data_file_5' => 'System _.1/Category Course category 1 _.' . $categorycontext->id . '/data.js',
1321 'data_file_6' => 'System _.1/Logs/Standard log/data.js'
1323 $this->assertEquals($expectedindex, $indexdata);
1325 $richtree = phpunit_util::call_internal_method($writer, 'make_tree_object', [$tree, $treelist],
1326 '\core_privacy\local\request\moodle_content_writer');
1328 // This is a big one.
1329 $expectedrichtree = [
1330 'System _.1' => (object) [
1331 'itemtype' => 'treeitem',
1332 'name' => 'System ',
1333 'context' => \context_system::instance(),
1336 'name' => 'data.json',
1337 'itemtype' => 'item',
1338 'datavar' => 'data_file_1'
1340 'paper' => (object) [
1341 'itemtype' => 'treeitem',
1344 'data.json' => (object) [
1345 'name' => 'data.json',
1346 'itemtype' => 'item',
1347 'datavar' => 'data_file_2'
1351 'Category Miscellaneous _.' . $misccoursecxt->id => (object) [
1352 'itemtype' => 'treeitem',
1353 'name' => 'Category Miscellaneous ',
1354 'context' => $misccoursecxt,
1356 'Course Test course 1 _.' . $coursecontext->id => (object) [
1357 'itemtype' => 'treeitem',
1358 'name' => 'Course Test course 1 ',
1359 'context' => $coursecontext,
1361 'Chat Chat 1 _.' . $modulecontext->id => (object) [
1362 'itemtype' => 'treeitem',
1363 'name' => 'Chat Chat 1 ',
1364 'context' => $modulecontext,
1366 'data.json' => (object) [
1367 'name' => 'data.json',
1368 'itemtype' => 'item',
1369 'datavar' => 'data_file_3'
1373 'grades' => (object) [
1374 'itemtype' => 'treeitem',
1377 'data.json' => (object) [
1378 'name' => 'data.json',
1379 'itemtype' => 'item',
1380 'datavar' => 'data_file_4'
1388 'Category Course category 1 _.' . $categorycontext->id => (object) [
1389 'itemtype' => 'treeitem',
1390 'name' => 'Category Course category 1 ',
1391 'context' => $categorycontext,
1393 'data.json' => (object) [
1394 'name' => 'data.json',
1395 'itemtype' => 'item',
1396 'datavar' => 'data_file_5'
1400 '_files' => (object) [
1401 'itemtype' => 'treeitem',
1404 'tests' => (object) [
1405 'itemtype' => 'treeitem',
1408 'a.txt' => (object) [
1410 'itemtype' => 'item',
1411 'url' => new \moodle_url('System _.1/_files/tests/a.txt')
1417 'Logs' => (object) [
1418 'itemtype' => 'treeitem',
1421 'Standard log' => (object) [
1422 'itemtype' => 'treeitem',
1423 'name' => 'Standard log',
1425 'data.json' => (object) [
1426 'name' => 'data.json',
1427 'itemtype' => 'item',
1428 'datavar' => 'data_file_6'
1437 $this->assertEquals($expectedrichtree, $richtree);
1439 // The phpunit_util::call_internal_method() method doesn't allow for referenced parameters so we have this joyful code
1440 // instead to do the same thing, but with references working obviously.
1441 $funfunction = function($object, $data) {
1442 return $object->sort_my_list($data);
1445 $funfunction = Closure::bind($funfunction, null, $writer);
1446 $funfunction($writer, $richtree);
1448 // This is a big one.
1449 $expectedsortedtree = [
1450 'System _.1' => (object) [
1451 'itemtype' => 'treeitem',
1452 'name' => 'System ',
1453 'context' => \context_system::instance(),
1455 'Category Miscellaneous _.' . $misccoursecxt->id => (object) [
1456 'itemtype' => 'treeitem',
1457 'name' => 'Category Miscellaneous ',
1458 'context' => $misccoursecxt,
1460 'Course Test course 1 _.' . $coursecontext->id => (object) [
1461 'itemtype' => 'treeitem',
1462 'name' => 'Course Test course 1 ',
1463 'context' => $coursecontext,
1465 'Chat Chat 1 _.' . $modulecontext->id => (object) [
1466 'itemtype' => 'treeitem',
1467 'name' => 'Chat Chat 1 ',
1468 'context' => $modulecontext,
1470 'data.json' => (object) [
1471 'name' => 'data.json',
1472 'itemtype' => 'item',
1473 'datavar' => 'data_file_3'
1477 'grades' => (object) [
1478 'itemtype' => 'treeitem',
1481 'data.json' => (object) [
1482 'name' => 'data.json',
1483 'itemtype' => 'item',
1484 'datavar' => 'data_file_4'
1492 'Category Course category 1 _.' . $categorycontext->id => (object) [
1493 'itemtype' => 'treeitem',
1494 'name' => 'Category Course category 1 ',
1495 'context' => $categorycontext,
1497 'data.json' => (object) [
1498 'name' => 'data.json',
1499 'itemtype' => 'item',
1500 'datavar' => 'data_file_5'
1504 '_files' => (object) [
1505 'itemtype' => 'treeitem',
1508 'tests' => (object) [
1509 'itemtype' => 'treeitem',
1512 'a.txt' => (object) [
1514 'itemtype' => 'item',
1515 'url' => new \moodle_url('System _.1/_files/tests/a.txt')
1521 'Logs' => (object) [
1522 'itemtype' => 'treeitem',
1525 'Standard log' => (object) [
1526 'itemtype' => 'treeitem',
1527 'name' => 'Standard log',
1529 'data.json' => (object) [
1530 'name' => 'data.json',
1531 'itemtype' => 'item',
1532 'datavar' => 'data_file_6'
1538 'paper' => (object) [
1539 'itemtype' => 'treeitem',
1542 'data.json' => (object) [
1543 'name' => 'data.json',
1544 'itemtype' => 'item',
1545 'datavar' => 'data_file_2'
1550 'name' => 'data.json',
1551 'itemtype' => 'item',
1552 'datavar' => 'data_file_1'
1557 $this->assertEquals($expectedsortedtree, $richtree);