MDL-62285 privacy: use context id when generating context path
[moodle.git] / privacy / tests / moodle_content_writer_test.php
blob7b49ef9dc45fe1a06f533092a054c19fc8fe6296
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.
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.
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
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.
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 {
41 /**
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();
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);
65 /**
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);
89 /**
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();
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);
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) [
143 'abc' => 'def',
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);
167 * Data provider for exporting user data.
169 public function export_data_provider() {
170 return [
171 'basic' => [
172 (object) [
173 'example' => (object) [
174 'key' => 'value',
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();
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);
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);
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);
295 * Data provider for exporting user metadata.
297 * return array
299 public function export_metadata_provider() {
300 return [
301 'basic' => [
302 'key',
303 'value',
304 'This is a description',
306 'valuewithspaces' => [
307 'key',
308 'value has mixed',
309 'This is a description',
311 'encodedvalue' => [
312 'key',
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.
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',
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',
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',
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',
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',
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,
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 ?: '') . $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,
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.
457 * @return array
459 public function export_file_provider() {
460 return [
461 'basic' => [
462 'intro',
464 '/',
465 'testfile.txt',
466 'An example file content',
468 'longpath' => [
469 'attachments',
470 '12',
471 '/path/within/a/path/within/a/path/',
472 'testfile.txt',
473 'An example file content',
475 'pathwithspaces' => [
476 'intro',
478 '/path with/some spaces/',
479 'testfile.txt',
480 'An example file content',
482 'filewithspaces' => [
483 'submission_attachments',
485 '/path with/some spaces/',
486 'test file.txt',
487 'An example file content',
489 'image' => [
490 'intro',
492 '/',
493 'logo.png',
494 file_get_contents(__DIR__ . '/fixtures/logo.png'),
496 'UTF8' => [
497 'submission_content',
499 '/Žluťoučký/',
500 'koníček.txt',
501 'koníček',
503 'EUC-JP' => [
504 'intro',
506 '/言語設定/',
507 '言語設定.txt',
508 '言語設定',
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) {
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);
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) {
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);
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) {
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);
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) {
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);
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.
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';
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);
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';
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);
828 * Provider for various user preferences.
830 * @return array
832 public function export_user_preference_provider() {
833 return [
834 'basic' => [
835 'core_privacy',
836 'onekey',
837 'value',
838 'description',
840 'encodedvalue' => [
841 'core_privacy',
842 'donkey',
843 base64_encode('value'),
844 'description',
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.',
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();
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);
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/", $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();
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);
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/", $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.
964 * @return array
966 public function unescaped_unicode_export_provider() {
967 return [
968 'Unicode' => ['ةكءيٓ‌پچژکگیٹڈڑہھےâîûğŞAaÇÖáǽ你好!'],
973 * Get a fresh content writer.
975 * @return moodle_content_writer
977 public function get_writer_instance() {
978 $factory = $this->createMock(writer::class);
979 return new moodle_content_writer($factory);
983 * Fetch the exported content for inspection.
985 * @param moodle_content_writer $writer
986 * @return \org\bovigo\vfs\vfsStreamDirectory
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;
1003 * Determine the path for the current context.
1005 * Note: This is a wrapper around the real function.
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.
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);
1029 * Test correct rewriting of @@PLUGINFILE@@ in the exported contents.
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.
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);
1048 * Provides testable sample data for {@link self::test_rewrite_pluginfile_urls()}.
1050 * @return array
1052 public function rewrite_pluginfile_urls_provider() {
1053 return [
1054 'zeroitemid' => [
1055 'intro',
1057 '<p><img src="@@PLUGINFILE@@/hello.gif" /></p>',
1058 '<p><img src="_files/intro/hello.gif" /></p>',
1060 'nonzeroitemid' => [
1061 'submission_content',
1063 '<p><img src="@@PLUGINFILE@@/first.png" alt="First" /></p>',
1064 '<p><img src="_files/submission_content/34/first.png" alt="First" /></p>',
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>',