diff --git a/privacy/classes/tests/request/content_writer.php b/privacy/classes/tests/request/content_writer.php index 5ae8a2868cd..78c1c62624a 100644 --- a/privacy/classes/tests/request/content_writer.php +++ b/privacy/classes/tests/request/content_writer.php @@ -141,13 +141,17 @@ class content_writer implements \core_privacy\local\request\content_writer { * @param \stdClass $data The data to be exported */ public function export_data(array $subcontext, \stdClass $data) : \core_privacy\local\request\content_writer { - array_push($subcontext, 'data'); - - $finalcontent = $data; + $finalcontent = [ + 'children' => [], + 'data' => $data, + ]; while ($pathtail = array_pop($subcontext)) { $finalcontent = [ - $pathtail => $finalcontent, + 'children' => [ + $pathtail => $finalcontent, + ], + 'data' => [], ]; } @@ -165,10 +169,8 @@ class content_writer implements \core_privacy\local\request\content_writer { public function get_data(array $subcontext = []) { $basepath = $this->data[$this->context->id]; while ($subpath = array_shift($subcontext)) { - if (isset($basepath[$subpath])) { - $basepath = $basepath[$subpath]; - } else { - return []; + if (isset($basepath['children']) && isset($basepath['children'][$subpath])) { + $basepath = $basepath['children'][$subpath]; } } @@ -195,18 +197,22 @@ class content_writer implements \core_privacy\local\request\content_writer { $value, string $description ) : \core_privacy\local\request\content_writer { - array_push($subcontext, 'metadata'); - $finalcontent = [ - $key => (object) [ - 'value' => $value, - 'description' => $description, + 'children' => [], + 'metadata' => [ + $key => (object) [ + 'value' => $value, + 'description' => $description, + ], ], ]; while ($pathtail = array_pop($subcontext)) { $finalcontent = [ - $pathtail => $finalcontent, + 'children' => [ + $pathtail => $finalcontent, + ], + 'metadata' => [], ]; } @@ -224,8 +230,8 @@ class content_writer implements \core_privacy\local\request\content_writer { public function get_all_metadata(array $subcontext = []) { $basepath = $this->metadata[$this->context->id]; while ($subpath = array_shift($subcontext)) { - if (isset($basepath[$subpath])) { - $basepath = $basepath[$subpath]; + if (isset($basepath['children']) && isset($basepath['children'][$subpath])) { + $basepath = $basepath['children'][$subpath]; } } @@ -245,13 +251,14 @@ class content_writer implements \core_privacy\local\request\content_writer { * @return array The metadata as a series of keys to value + descrition objects. */ public function get_metadata(array $subcontext = [], $key, $valueonly = true) { - $data = $this->get_all_metadata($subcontext); + $keys = $this->get_all_metadata($subcontext); - if (!isset($data[$key])) { + if (isset($keys[$key])) { + $metadata = $keys[$key]; + } else { return null; } - $metadata = $data[$key]; if ($valueonly) { return $metadata->value; } else { @@ -267,14 +274,19 @@ class content_writer implements \core_privacy\local\request\content_writer { * @param \stdClass $data The related data to export. */ public function export_related_data(array $subcontext, $name, $data) : \core_privacy\local\request\content_writer { - array_push($subcontext, $name); - array_push($subcontext, 'data'); - - $finalcontent = $data; + $finalcontent = [ + 'children' => [], + 'data' => [ + $name => $data, + ], + ]; while ($pathtail = array_pop($subcontext)) { $finalcontent = [ - $pathtail => $finalcontent, + 'children' => [ + $pathtail => $finalcontent, + ], + 'data' => [], ]; } @@ -290,22 +302,27 @@ class content_writer implements \core_privacy\local\request\content_writer { * @param string $filename The name of the intended filename. * @return array The metadata as a series of keys to value + descrition objects. */ - public function get_related_data(array $subcontext = [], $filename) { + public function get_related_data(array $subcontext = [], $filename = null) { $basepath = $this->relateddata[$this->context->id]; - $subcontext[] = $filename; while ($subpath = array_shift($subcontext)) { - if (isset($basepath[$subpath])) { - $basepath = $basepath[$subpath]; - } else { - return []; + if (isset($basepath['children']) && isset($basepath['children'][$subpath])) { + $basepath = $basepath['children'][$subpath]; } } if (isset($basepath['data'])) { - return $basepath['data']; - } else { - return []; + $data = $basepath['data']; + if (null !== $filename) { + if (isset($data[$filename])) { + return $data[$filename]; + } + return null; + } + + return $data; } + + return null; } /** @@ -319,11 +336,17 @@ class content_writer implements \core_privacy\local\request\content_writer { $filename = clean_param($filename, PARAM_FILE); $finalcontent = [ - $filename => $filecontent, + 'children' => [], + 'files' => [ + $filename => $filecontent, + ], ]; while ($pathtail = array_pop($subcontext)) { $finalcontent = [ - $pathtail => $finalcontent, + 'children' => [ + $pathtail => $finalcontent, + ], + 'files' => [], ]; } @@ -340,18 +363,23 @@ class content_writer implements \core_privacy\local\request\content_writer { * @return string The content of the file. */ public function get_custom_file(array $subcontext = [], $filename = null) { - if (!empty($filename)) { - array_push($subcontext, $filename); - } - $basepath = $this->customfiles[$this->context->id]; while ($subpath = array_shift($subcontext)) { - if (isset($basepath[$subpath])) { - $basepath = $basepath[$subpath]; + if (isset($basepath['children']) && isset($basepath['children'][$subpath])) { + $basepath = $basepath['children'][$subpath]; } } - return $basepath; + if (isset($basepath['files'])) { + if (null !== $filename) { + if (isset($basepath['files'][$filename])) { + return $basepath['files'][$filename]; + } + return null; + } + return $basepath['files']; + } + return null; } /** @@ -394,18 +422,24 @@ class content_writer implements \core_privacy\local\request\content_writer { */ public function export_file(array $subcontext, \stored_file $file) : \core_privacy\local\request\content_writer { if (!$file->is_directory()) { - $subcontextextra = [ - 'files', - $file->get_filepath(), - ]; - $newsubcontext = array_merge($subcontext, $subcontextextra); + $filepath = explode(DIRECTORY_SEPARATOR, $file->get_filepath()); + $filepath[] = $file->get_filename(); + $filepath = array_filter($filepath); + $filepath = implode('/', $filepath); $finalcontent = [ - $file, + 'children' => [], + 'files' => [ + $filepath => $file, + ], ]; + while ($pathtail = array_pop($subcontext)) { $finalcontent = [ - $pathtail => $finalcontent, + 'children' => [ + $pathtail => $finalcontent, + ], + 'files' => [], ]; } @@ -424,12 +458,16 @@ class content_writer implements \core_privacy\local\request\content_writer { public function get_files(array $subcontext = []) { $basepath = $this->files[$this->context->id]; while ($subpath = array_shift($subcontext)) { - if (isset($basepath[$subpath])) { - $basepath = $basepath[$subpath]; + if (isset($basepath['children']) && isset($basepath['children'][$subpath])) { + $basepath = $basepath['children'][$subpath]; } } - return $basepath; + if (isset($basepath['files'])) { + return $basepath['files']; + } + + return []; } /** diff --git a/privacy/tests/tests_content_writer_test.php b/privacy/tests/tests_content_writer_test.php new file mode 100644 index 00000000000..1f815c11434 --- /dev/null +++ b/privacy/tests/tests_content_writer_test.php @@ -0,0 +1,361 @@ +. + +/** + * Unit Tests for the Content Writer used for unit testing. + * + * @package core_privacy + * @category test + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +use \core_privacy\local\request\writer; +use \core_privacy\tests\request\content_writer; + +/** + * Unit Tests for the Content Writer used for unit testing. + * + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class tests_content_writer_test extends advanced_testcase { + + /** + * It should be possible to store and retrieve data. + */ + public function test_export_data() { + $context = \context_system::instance(); + $writer = $this->get_writer_instance(); + + $dataa = (object) [ + 'example' => 'a', + ]; + $datab = (object) [ + 'example' => 'b', + ]; + + $writer->set_context($context) + ->export_data(['data'], $dataa) + ->export_data([], $datab); + + $data = $writer->get_data([]); + $this->assertSame($datab, $data); + + $data = $writer->get_data(['data']); + $this->assertSame($dataa, $data); + } + + /** + * It should be possible to store and retrieve data at the same point in different contexts. + */ + public function test_export_data_no_context_clash() { + $writer = $this->get_writer_instance(); + + $context = \context_system::instance(); + $dataa = (object) [ + 'example' => 'a', + ]; + $writer->set_context($context) + ->export_data(['data'], $dataa); + + $adminuser = \core_user::get_user_by_username('admin'); + $usercontext = \context_user::instance($adminuser->id); + $datab = (object) [ + 'example' => 'b', + ]; + $writer->set_context($usercontext) + ->export_data(['data'], $datab); + + $writer->set_context($context); + $data = $writer->get_data(['data']); + $this->assertSame($dataa, $data); + + $writer->set_context($usercontext); + $data = $writer->get_data(['data']); + $this->assertSame($datab, $data); + } + + /** + * It should be possible to store and retrieve metadata. + */ + public function test_export_metadata() { + $context = \context_system::instance(); + $writer = $this->get_writer_instance(); + + $writer->set_context($context) + ->export_metadata(['metadata'], 'somekey', 'value1', 'description1') + ->export_metadata([], 'somekey', 'value2', 'description2'); + + $allmetadata = $writer->get_all_metadata([]); + $this->assertCount(1, $allmetadata); + $this->assertArrayHasKey('somekey', $allmetadata); + $this->assertEquals('value2', $allmetadata['somekey']->value); + $this->assertEquals('description2', $allmetadata['somekey']->description); + + $metadata = $writer->get_metadata([], 'somekey', false); + $this->assertEquals('value2', $metadata->value); + $this->assertEquals('description2', $metadata->description); + $this->assertEquals('value2', $writer->get_metadata([], 'somekey', true)); + + $allmetadata = $writer->get_all_metadata(['metadata']); + $this->assertCount(1, $allmetadata); + $this->assertArrayHasKey('somekey', $allmetadata); + $this->assertEquals('value1', $allmetadata['somekey']->value); + $this->assertEquals('description1', $allmetadata['somekey']->description); + + $metadata = $writer->get_metadata(['metadata'], 'somekey', false); + $this->assertEquals('value1', $metadata->value); + $this->assertEquals('description1', $metadata->description); + $this->assertEquals('value1', $writer->get_metadata(['metadata'], 'somekey', true)); + } + + /** + * It should be possible to store and retrieve metadata at the same point in different contexts. + */ + public function test_export_metadata_no_context_clash() { + $writer = $this->get_writer_instance(); + + $context = \context_system::instance(); + $writer->set_context($context) + ->export_metadata(['metadata'], 'somekey', 'value1', 'description1'); + + $adminuser = \core_user::get_user_by_username('admin'); + $usercontext = \context_user::instance($adminuser->id); + $writer->set_context($usercontext) + ->export_metadata(['metadata'], 'somekey', 'value2', 'description2'); + + $writer->set_context($context); + $allmetadata = $writer->get_all_metadata(['metadata']); + $this->assertCount(1, $allmetadata); + $this->assertArrayHasKey('somekey', $allmetadata); + $this->assertEquals('value1', $allmetadata['somekey']->value); + $this->assertEquals('description1', $allmetadata['somekey']->description); + + $metadata = $writer->get_metadata(['metadata'], 'somekey', false); + $this->assertEquals('value1', $metadata->value); + $this->assertEquals('description1', $metadata->description); + $this->assertEquals('value1', $writer->get_metadata(['metadata'], 'somekey', true)); + + $writer->set_context($usercontext); + $allmetadata = $writer->get_all_metadata(['metadata']); + $this->assertCount(1, $allmetadata); + $this->assertArrayHasKey('somekey', $allmetadata); + $this->assertEquals('value2', $allmetadata['somekey']->value); + $this->assertEquals('description2', $allmetadata['somekey']->description); + + $metadata = $writer->get_metadata(['metadata'], 'somekey', false); + $this->assertEquals('value2', $metadata->value); + $this->assertEquals('description2', $metadata->description); + $this->assertEquals('value2', $writer->get_metadata(['metadata'], 'somekey', true)); + } + + /** + * It should be possible to export files in the files and children contexts. + */ + public function test_export_file_special_folders() { + $context = \context_system::instance(); + + $filea = $this->get_stored_file('/', 'files'); + $fileb = $this->get_stored_file('/children/', 'foo.zip'); + + $writer = $this->get_writer_instance() + ->set_context($context) + ->export_file([], $filea) + ->export_file([], $fileb); + + $files = $writer->get_files([]); + + $this->assertCount(2, $files); + $this->assertEquals($filea, $files['files']); + $this->assertEquals($fileb, $files['children/foo.zip']); + } + + /** + * It should be possible to export mutliple files in the same subcontext/path space but different context and not + * have them clash. + */ + public function test_export_file_no_context_clash() { + $writer = $this->get_writer_instance(); + $context = \context_system::instance(); + $filea = $this->get_stored_file('/foo/', 'foo.txt'); + $writer = $this->get_writer_instance() + ->set_context($context) + ->export_file([], $filea); + + $adminuser = \core_user::get_user_by_username('admin'); + $usercontext = \context_user::instance($adminuser->id); + $fileb = $this->get_stored_file('/foo/', 'foo.txt'); + $writer->set_context($usercontext) + ->export_file([], $fileb); + + $writer->set_context($context); + $files = $writer->get_files([]); + $this->assertCount(1, $files); + $this->assertEquals($filea, $files['foo/foo.txt']); + + $writer->set_context($usercontext); + $files = $writer->get_files([]); + $this->assertCount(1, $files); + $this->assertEquals($fileb, $files['foo/foo.txt']); + } + + /** + * It should be possible to export related data in the files and children contexts. + */ + public function test_export_related_data() { + $context = \context_system::instance(); + + $writer = $this->get_writer_instance() + ->set_context($context) + ->export_related_data(['file', 'data'], 'file', 'data1') + ->export_related_data([], 'file', 'data2'); + + $data = $writer->get_related_data([]); + $this->assertCount(1, $data); + $this->assertEquals('data2', $data['file']); + + $data = $writer->get_related_data([], 'file'); + $this->assertEquals('data2', $data); + + $data = $writer->get_related_data(['file', 'data']); + $this->assertCount(1, $data); + $this->assertEquals('data1', $data['file']); + + $data = $writer->get_related_data(['file', 'data'], 'file'); + $this->assertEquals('data1', $data); + } + + /** + * It should be possible to export related data in the same location,but in a different context. + */ + public function test_export_related_data_no_context_clash() { + $writer = $this->get_writer_instance(); + + $context = \context_system::instance(); + $writer->set_context($context) + ->export_related_data(['file', 'data'], 'file', 'data1'); + + $adminuser = \core_user::get_user_by_username('admin'); + $usercontext = \context_user::instance($adminuser->id); + $writer->set_context($usercontext) + ->export_related_data(['file', 'data'], 'file', 'data2'); + + $writer->set_context($context); + $data = $writer->get_related_data(['file', 'data']); + $this->assertCount(1, $data); + $this->assertEquals('data1', $data['file']); + + $writer->set_context($usercontext); + $data = $writer->get_related_data(['file', 'data']); + $this->assertCount(1, $data); + $this->assertEquals('data2', $data['file']); + } + + /** + * It should be possible to export related files in the files and children contexts. + */ + public function test_export_custom_file() { + $context = \context_system::instance(); + + $writer = $this->get_writer_instance() + ->set_context($context) + ->export_custom_file(['file.txt'], 'file.txt', 'Content 1') + ->export_custom_file([], 'file.txt', 'Content 2'); + + $files = $writer->get_custom_file([]); + $this->assertCount(1, $files); + $this->assertEquals('Content 2', $files['file.txt']); + $file = $writer->get_custom_file([], 'file.txt'); + $this->assertEquals('Content 2', $file); + + $files = $writer->get_custom_file(['file.txt']); + $this->assertCount(1, $files); + $this->assertEquals('Content 1', $files['file.txt']); + $file = $writer->get_custom_file(['file.txt'], 'file.txt'); + $this->assertEquals('Content 1', $file); + } + + /** + * It should be possible to export related files in the same location + * in different contexts. + */ + public function test_export_custom_file_no_context_clash() { + $writer = $this->get_writer_instance(); + $context = \context_system::instance(); + + $writer->set_context($context) + ->export_custom_file(['file.txt'], 'file.txt', 'Content 1'); + + $adminuser = \core_user::get_user_by_username('admin'); + $usercontext = \context_user::instance($adminuser->id); + $writer->set_context($usercontext) + ->export_custom_file(['file.txt'], 'file.txt', 'Content 2'); + + $writer->set_context($context); + $files = $writer->get_custom_file(['file.txt']); + $this->assertCount(1, $files); + $this->assertEquals('Content 1', $files['file.txt']); + + $writer->set_context($usercontext); + $files = $writer->get_custom_file(['file.txt']); + $this->assertCount(1, $files); + $this->assertEquals('Content 2', $files['file.txt']); + } + + /** + * Get a fresh content writer. + * + * @return moodle_content_writer + */ + public function get_writer_instance() { + $factory = $this->createMock(writer::class); + return new content_writer($factory); + } + + /** + * Helper to create a stored file objectw with the given supplied content. + * + * @param string $filepath The file path to use in the stored_file + * @param string $filename The file name to use in the stored_file + * @return stored_file + */ + protected function get_stored_file($filepath, $filename) { + static $counter = 0; + $counter++; + $filecontent = "Example content {$counter}"; + $contenthash = file_storage::hash_from_string($filecontent); + + $file = $this->getMockBuilder(stored_file::class) + ->setMethods(null) + ->setConstructorArgs([ + get_file_storage(), + (object) [ + 'contenthash' => $contenthash, + 'filesize' => strlen($filecontent), + 'filepath' => $filepath, + 'filename' => $filename, + ] + ]) + ->getMock(); + + return $file; + } +}