diff --git a/mod/wiki/classes/privacy/provider.php b/mod/wiki/classes/privacy/provider.php new file mode 100644 index 0000000000000..32bf11fe205cd --- /dev/null +++ b/mod/wiki/classes/privacy/provider.php @@ -0,0 +1,494 @@ +. + +/** + * Data provider. + * + * @package mod_wiki + * @copyright 2018 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_wiki\privacy; + +use core_privacy\local\metadata\collection; +use core_privacy\local\request\approved_contextlist; +use core_privacy\local\request\contextlist; +use context_user; +use context; +use core_privacy\local\request\helper; +use core_privacy\local\request\transform; +use core_privacy\local\request\writer; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Data provider class. + * + * @package mod_wiki + * @copyright 2018 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements + \core_privacy\local\metadata\provider, + \core_privacy\local\request\plugin\provider { + + /** + * Returns metadata. + * + * @param collection $collection The initialised collection to add items to. + * @return collection A listing of user data stored through this system. + */ + public static function get_metadata(collection $collection) { + + $collection->add_database_table('wiki_subwikis', [ + 'userid' => 'privacy:metadata:wiki_subwikis:userid', + 'groupid' => 'privacy:metadata:wiki_subwikis:groupid', + ], 'privacy:metadata:wiki_subwikis'); + + $collection->add_database_table('wiki_pages', [ + 'userid' => 'privacy:metadata:wiki_pages:userid', + 'title' => 'privacy:metadata:wiki_pages:title', + 'cachedcontent' => 'privacy:metadata:wiki_pages:cachedcontent', + 'timecreated' => 'privacy:metadata:wiki_pages:timecreated', + 'timemodified' => 'privacy:metadata:wiki_pages:timemodified', + 'timerendered' => 'privacy:metadata:wiki_pages:timerendered', + 'pageviews' => 'privacy:metadata:wiki_pages:pageviews', + 'readonly' => 'privacy:metadata:wiki_pages:readonly', + ], 'privacy:metadata:wiki_pages'); + + $collection->add_database_table('wiki_versions', [ + 'userid' => 'privacy:metadata:wiki_versions:userid', + 'content' => 'privacy:metadata:wiki_versions:content', + 'contentformat' => 'privacy:metadata:wiki_versions:contentformat', + 'version' => 'privacy:metadata:wiki_versions:version', + 'timecreated' => 'privacy:metadata:wiki_versions:timecreated', + ], 'privacy:metadata:wiki_versions'); + + $collection->add_database_table('wiki_locks', [ + 'userid' => 'privacy:metadata:wiki_locks:userid', + 'sectionname' => 'privacy:metadata:wiki_locks:sectionname', + 'lockedat' => 'privacy:metadata:wiki_locks:lockedat', + ], 'privacy:metadata:wiki_locks'); + + $collection->link_subsystem('core_files', 'privacy:metadata:core_files'); + $collection->link_subsystem('core_tag', 'privacy:metadata:core_tag'); + $collection->link_subsystem('core_comment', 'privacy:metadata:core_comment'); + + // We do not report on wiki, wiki_synonyms, wiki_links because this is just context-related data. + + return $collection; + } + + /** + * Get the list of contexts that contain user information for the specified user. + * + * @param int $userid The user to search. + * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin. + */ + public static function get_contexts_for_userid($userid) { + $contextlist = new contextlist(); + + $contextlist->add_from_sql('SELECT ctx.id + FROM {modules} m + JOIN {course_modules} cm ON cm.module = m.id AND m.name = :modname + JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :contextlevel + JOIN {wiki_subwikis} s ON cm.instance = s.wikiid + LEFT JOIN {wiki_pages} p ON p.subwikiid = s.id + LEFT JOIN {wiki_versions} v ON v.pageid = p.id AND v.userid = :userid3 + LEFT JOIN {wiki_locks} l ON l.pageid = p.id AND l.userid = :userid4 + LEFT JOIN {comments} com ON com.itemid = p.id AND com.commentarea = :commentarea + AND com.contextid = ctx.id AND com.userid = :userid5 + WHERE s.userid = :userid1 OR p.userid = :userid2 OR v.id IS NOT NULL OR l.id IS NOT NULL OR com.id IS NOT NULL', + ['modname' => 'wiki', 'contextlevel' => CONTEXT_MODULE, 'userid1' => $userid, 'userid2' => $userid, + 'userid3' => $userid, 'userid4' => $userid, 'commentarea' => 'wiki_page', 'userid5' => $userid]); + + return $contextlist; + } + + /** + * Add one subwiki to the export + * + * Each page is added as related data because all pages in one subwiki share the same filearea + * + * @param stdClass $user + * @param context $context + * @param array $subwiki + * @param string $wikimode + */ + protected static function export_subwiki($user, context $context, $subwiki, $wikimode) { + if (empty($subwiki)) { + return; + } + $subwikiid = key($subwiki); + $pages = $subwiki[$subwikiid]['pages']; + unset($subwiki[$subwikiid]['pages']); + writer::with_context($context)->export_data([$subwikiid], (object)$subwiki[$subwikiid]); + $allfiles = $wikimode === 'individual'; // Whether to export all files or only the ones that are used. + + $alltexts = ''; // Store all texts that reference files to search which files are used. + foreach ($pages as $page => $entry) { + // Preprocess current page contents. + if (!$allfiles && self::text_has_files($entry['page']['cachedcontent'])) { + $alltexts .= $entry['page']['cachedcontent']; + } + $entry['page']['cachedcontent'] = format_text(writer::with_context($context) + ->rewrite_pluginfile_urls([$subwikiid], 'mod_wiki', 'attachments', + $subwikiid, $entry['page']['cachedcontent']), FORMAT_HTML, ['context' => $context]); + // Add page tags. + $pagetags = \core_tag_tag::get_item_tags_array('mod_wiki', 'page', $entry['page']['id']); + if ($pagetags) { + $entry['page']['tags'] = $pagetags; + } + + // Preprocess revisions. + if (!empty($entry['revisions'])) { + // For each revision this user has made preprocess the contents. + foreach ($entry['revisions'] as &$revision) { + if ((!$allfiles && self::text_has_files($revision['content']))) { + $alltexts .= $revision['content']; + } + $revision['content'] = writer::with_context($context) + ->rewrite_pluginfile_urls([$subwikiid], 'mod_wiki', 'attachments', $subwikiid, $revision['content']); + } + } + $comments = self::get_page_comments($user, $context, $entry['page']['id'], !array_key_exists('userid', $entry['page'])); + if ($comments) { + $entry['page']['comments'] = $comments; + } + writer::with_context($context)->export_related_data([$subwikiid], $page, $entry); + } + + if ($allfiles) { + // Export all files. + writer::with_context($context)->export_area_files([$subwikiid], 'mod_wiki', 'attachments', $subwikiid); + } else { + // Analyze which files are used in the texts. + self::export_used_files($context, $subwikiid, $alltexts); + } + } + + /** + * Retrieves page comments + * + * We can not use \core_comment\privacy\provider::export_comments() because it expects each item to have a separate + * subcontext and we store wiki pages as related data to subwiki because the files are shared between pages. + * + * @param stdClass $user + * @param \context $context + * @param int $pageid + * @param bool $onlyforthisuser + * @return array + */ + protected static function get_page_comments($user, \context $context, $pageid, $onlyforthisuser = true) { + global $USER, $DB; + $params = [ + 'contextid' => $context->id, + 'commentarea' => 'wiki_page', + 'itemid' => $pageid + ]; + $sql = "SELECT c.id, c.content, c.format, c.timecreated, c.userid + FROM {comments} c + WHERE c.contextid = :contextid AND + c.commentarea = :commentarea AND + c.itemid = :itemid"; + if ($onlyforthisuser) { + $sql .= " AND c.userid = :userid"; + $params['userid'] = $USER->id; + } + $sql .= " ORDER BY c.timecreated DESC"; + + $rs = $DB->get_recordset_sql($sql, $params); + $comments = []; + foreach ($rs as $record) { + if ($record->userid != $user->id) { + // Clean HTML in comments that were added by other users. + $comment = ['content' => format_text($record->content, $record->format, ['context' => $context])]; + } else { + // Export comments made by this user as they are stored. + $comment = ['content' => $record->content, 'contentformat' => $record->format]; + } + $comment += [ + 'time' => transform::datetime($record->timecreated), + 'userid' => transform::user($record->userid), + ]; + $comments[] = (object)$comment; + } + $rs->close(); + return $comments; + } + + /** + * Check if text has embedded files + * + * @param string $str + * @return bool + */ + protected static function text_has_files($str) { + return strpos($str, '@@PLUGINFILE@@') !== false; + } + + /** + * Analyze which files are used in the texts and export + * @param context $context + * @param int $subwikiid + * @param string $alltexts + * @return int|void + */ + protected static function export_used_files($context, $subwikiid, $alltexts) { + if (!self::text_has_files($alltexts)) { + return; + } + $fs = get_file_storage(); + $files = $fs->get_area_files($context->id, 'mod_wiki', 'attachments', $subwikiid, + 'filepath, filename', false); + if (empty($files)) { + return; + } + usort($files, function($file1, $file2) { + return strcmp($file2->get_filepath(), $file1->get_filename()); + }); + foreach ($files as $file) { + $filepath = $file->get_filepath() . $file->get_filename(); + $needles = ['@@PLUGINFILE@@' . s($filepath), + '@@PLUGINFILE@@' . $filepath, + '@@PLUGINFILE@@' . str_replace(' ', '%20', $filepath), + '@@PLUGINFILE@@' . s($filepath), + '@@PLUGINFILE@@' . s(str_replace(' ', '%20', $filepath)) + ]; + $needles = array_unique($needles); + $newtext = str_replace($needles, '', $alltexts); + if ($newtext !== $alltexts) { + $alltexts = $newtext; + writer::with_context($context)->export_file([$subwikiid], $file); + if (!self::text_has_files($alltexts)) { + return; + } + } + } + } + + /** + * Export all user data for the specified user, in the specified contexts. + * + * @param approved_contextlist $contextlist The approved contexts to export information for. + */ + public static function export_user_data(approved_contextlist $contextlist) { + global $DB; + + foreach ($contextlist as $context) { + if ($context->contextlevel != CONTEXT_MODULE) { + continue; + } + $user = $contextlist->get_user(); + + $rs = $DB->get_recordset_sql('SELECT w.wikimode, s.id AS subwikiid, + s.groupid AS subwikigroupid, s.userid AS subwikiuserid, + p.id AS pageid, p.userid AS pageuserid, p.title, p.cachedcontent, p.timecreated AS pagetimecreated, + p.timemodified AS pagetimemodified, p.timerendered AS pagetimerendered, p.pageviews, p.readonly, + v.id AS versionid, v.content, v.contentformat, v.version, v.timecreated AS versiontimecreated, + l.id AS lockid, l.sectionname, l.lockedat + FROM {course_modules} cm + JOIN {wiki} w ON w.id = cm.instance + JOIN {wiki_subwikis} s ON cm.instance = s.wikiid + LEFT JOIN {wiki_pages} p ON p.subwikiid = s.id + LEFT JOIN {wiki_versions} v ON v.pageid = p.id AND v.userid = :user4 + LEFT JOIN {wiki_locks} l ON l.pageid = p.id AND l.userid = :user5 + WHERE cm.id = :cmid AND (s.userid = :user1 OR p.userid = :user2 OR v.userid = :user3 OR l.userid = :user6 OR + EXISTS (SELECT 1 FROM {comments} com WHERE com.itemid = p.id AND com.commentarea = :commentarea + AND com.contextid = :ctxid AND com.userid = :user7) + ) + ORDER BY s.id, p.id, v.id', + ['cmid' => $context->instanceid, + 'user1' => $user->id, 'user2' => $user->id, 'user3' => $user->id, 'user4' => $user->id, + 'user5' => $user->id, 'user6' => $user->id, 'user7' => $user->id, 'commentarea' => 'wiki_page', + 'ctxid' => $context->id]); + + if (!$rs->current()) { + $rs->close(); + continue; + } + + $subwiki = []; + $wikimode = null; + foreach ($rs as $record) { + if ($wikimode === null) { + $wikimode = $record->wikimode; + } + if (!isset($subwiki[$record->subwikiid])) { + self::export_subwiki($user, $context, $subwiki, $wikimode); + $subwiki = [$record->subwikiid => [ + 'groupid' => $record->subwikigroupid, + 'userid' => $record->subwikiuserid ? transform::user($record->subwikiuserid) : 0, + 'pages' => [] + ]]; + } + + if (!$record->pageid) { + // This is an empty individual wiki. + continue; + } + + // Prepend page title with the page id to guarantee uniqueness. + $pagetitle = format_string($record->title, true, ['context' => $context]); + $page = $record->pageid . ' ' . $pagetitle; + if (!isset($subwiki[$record->subwikiid]['pages'][$page])) { + // Export basic details about the page. + $subwiki[$record->subwikiid]['pages'][$page] = ['page' => [ + 'id' => $record->pageid, + 'title' => $pagetitle, + 'cachedcontent' => $record->cachedcontent, + ]]; + if ($record->pageuserid == $user->id) { + // This page belongs to this user. Export all details. + $subwiki[$record->subwikiid]['pages'][$page]['page'] += [ + 'userid' => transform::user($user->id), + 'timecreated' => transform::datetime($record->pagetimecreated), + 'timemodified' => transform::datetime($record->pagetimemodified), + 'timerendered' => transform::datetime($record->pagetimerendered), + 'pageviews' => $record->pageviews, + 'readonly' => $record->readonly, + ]; + + $subwiki[$record->subwikiid]['pages'][$page]['page']['userid'] = transform::user($user->id); + } + } + + if ($record->versionid) { + $subwiki[$record->subwikiid]['pages'][$page]['revisions'][$record->versionid] = [ + 'content' => $record->content, + 'contentformat' => $record->contentformat, + 'version' => $record->version, + 'timecreated' => transform::datetime($record->versiontimecreated) + ]; + } + + if ($record->lockid) { + $subwiki[$record->subwikiid]['pages'][$page]['locks'][$record->lockid] = [ + 'sectionname' => $record->sectionname, + 'lockedat' => transform::datetime($record->lockedat), + ]; + } + + } + self::export_subwiki($user, $context, $subwiki, $wikimode); + + if ($subwiki) { + // Export wiki itself. + $contextdata = helper::get_context_data($context, $user); + helper::export_context_files($context, $user); + writer::with_context($context)->export_data([], $contextdata); + } + + $rs->close(); + } + } + + /** + * Delete all data for all users in the specified context. + * + * @param context $context The specific context to delete data for. + */ + public static function delete_data_for_all_users_in_context(context $context) { + global $DB; + + if ($context->contextlevel != CONTEXT_MODULE) { + return; + } + + $subwikis = $DB->get_fieldset_sql('SELECT s.id + FROM {course_modules} cm + JOIN {modules} m ON m.name = :wiki AND cm.module = m.id + JOIN {wiki_subwikis} s ON s.wikiid = cm.instance + WHERE cm.id = :cmid', + ['cmid' => $context->instanceid, 'wiki' => 'wiki']); + if (!$subwikis) { + return; + } + + $fs = get_file_storage(); + $fs->delete_area_files($context->id, 'mod_wiki', 'attachments'); + + \core_tag\privacy\provider::delete_item_tags($context, 'mod_wiki', 'page'); + + \core_comment\privacy\provider::delete_comments_for_all_users($context, 'mod_wiki', 'wiki_page'); + + list($sql, $params) = $DB->get_in_or_equal($subwikis); + $DB->delete_records_select('wiki_locks', 'pageid IN (SELECT id FROM {wiki_pages} WHERE subwikiid '.$sql.')', $params); + $DB->delete_records_select('wiki_versions', 'pageid IN (SELECT id FROM {wiki_pages} WHERE subwikiid '.$sql.')', $params); + $DB->delete_records_select('wiki_synonyms', 'subwikiid '.$sql, $params); + $DB->delete_records_select('wiki_links', 'subwikiid '.$sql, $params); + $DB->delete_records_select('wiki_pages', 'subwikiid '.$sql, $params); + $DB->delete_records_select('wiki_subwikis', 'id '.$sql, $params); + + $DB->delete_records('tag_instance', ['contextid' => $context->id, 'component' => 'mod_wiki', 'itemtype' => 'page']); + } + + /** + * Delete all user data for the specified user, in the specified contexts. + * + * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. + */ + public static function delete_data_for_user(approved_contextlist $contextlist) { + global $DB; + + $contextids = $contextlist->get_contextids(); + + if (!$contextids) { + return; + } + + // Remove only individual subwikis. Contributions to collaborative wikis is not considered personal contents. + list($ctxsql, $ctxparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED); + $subwikis = $DB->get_records_sql_menu('SELECT s.id, ctx.id AS ctxid + FROM {context} ctx + JOIN {course_modules} cm ON cm.id = ctx.instanceid AND ctx.contextlevel = :contextmod + JOIN {modules} m ON m.name = :wiki AND cm.module = m.id + JOIN {wiki_subwikis} s ON s.wikiid = cm.instance AND s.userid = :userid + WHERE ctx.id ' . $ctxsql, + ['userid' => (int)$contextlist->get_user()->id, 'wiki' => 'wiki', 'contextmod' => CONTEXT_MODULE] + $ctxparams); + + if ($subwikis) { + // We found individual subwikis that need to be deleted completely. + + $fs = get_file_storage(); + foreach ($subwikis as $subwikiid => $contextid) { + $fs->delete_area_files($contextid, 'mod_wiki', 'attachments', $subwikiid); + \core_comment\privacy\provider::delete_comments_for_all_users_select(context::instance_by_id($contextid), + 'mod_wiki', 'wiki_page', "IN (SELECT id FROM {wiki_pages} WHERE subwikiid=:subwikiid)", + ['subwikiid' => $subwikiid]); + } + + list($sql, $params) = $DB->get_in_or_equal(array_keys($subwikis), SQL_PARAMS_NAMED); + + $DB->execute("DELETE FROM {tag_instance} WHERE component=:component AND itemtype=:itemtype AND itemid IN + (SELECT id FROM {wiki_pages} WHERE subwikiid $sql)", + ['component' => 'mod_wiki', 'itemtype' => 'page'] + $params); + + $DB->delete_records_select('wiki_locks', 'pageid IN (SELECT id FROM {wiki_pages} WHERE subwikiid ' . $sql . ')', + $params); + $DB->delete_records_select('wiki_versions', 'pageid IN (SELECT id FROM {wiki_pages} WHERE subwikiid ' . $sql . ')', + $params); + $DB->delete_records_select('wiki_synonyms', 'subwikiid ' . $sql, $params); + $DB->delete_records_select('wiki_links', 'subwikiid ' . $sql, $params); + $DB->delete_records_select('wiki_pages', 'subwikiid ' . $sql, $params); + $DB->delete_records_select('wiki_subwikis', 'id ' . $sql, $params); + } + + // Remove comments made by this user on all other wiki pages. + \core_comment\privacy\provider::delete_comments_for_user($contextlist, 'mod_wiki', 'wiki_page'); + } +} diff --git a/mod/wiki/lang/en/wiki.php b/mod/wiki/lang/en/wiki.php index 329bc77a16ab3..faa7e40cc8fe8 100644 --- a/mod/wiki/lang/en/wiki.php +++ b/mod/wiki/lang/en/wiki.php @@ -195,6 +195,31 @@ $string['pluginname'] = 'Wiki'; $string['prettyprint'] = 'Printer-friendly version'; $string['print'] = 'Print'; +$string['privacy:metadata:core_comment'] = 'Comments on wiki page'; +$string['privacy:metadata:core_files'] = 'Files attached to subwikis'; +$string['privacy:metadata:core_tag'] = 'Tags associated with wiki pages'; +$string['privacy:metadata:wiki_locks'] = 'Temporary storage for wiki edit locks'; +$string['privacy:metadata:wiki_locks:userid'] = 'User who locked a page'; +$string['privacy:metadata:wiki_locks:sectionname'] = 'Name of the locked page section'; +$string['privacy:metadata:wiki_locks:lockedat'] = 'Date when locked'; +$string['privacy:metadata:wiki_pages'] = 'Information about wiki pages'; +$string['privacy:metadata:wiki_pages:userid'] = 'Last user who edited the page'; +$string['privacy:metadata:wiki_pages:title'] = 'Name of the page'; +$string['privacy:metadata:wiki_pages:cachedcontent'] = 'Cached content in HTML format'; +$string['privacy:metadata:wiki_pages:timecreated'] = 'Time when page was first created'; +$string['privacy:metadata:wiki_pages:timemodified'] = 'Time when page was last modified'; +$string['privacy:metadata:wiki_pages:timerendered'] = 'Time when page was last rendered'; +$string['privacy:metadata:wiki_pages:pageviews'] = 'Number of times page was viewed'; +$string['privacy:metadata:wiki_pages:readonly'] = 'Whether a page is read-only'; +$string['privacy:metadata:wiki_subwikis'] = 'Information about subwikis (in case of group or individual mode)'; +$string['privacy:metadata:wiki_subwikis:userid'] = 'User who owns a subwiki (for individual wikis)'; +$string['privacy:metadata:wiki_subwikis:groupid'] = 'Group that owns a subwiki'; +$string['privacy:metadata:wiki_versions'] = 'Information about wiki pages history'; +$string['privacy:metadata:wiki_versions:userid'] = 'User who created revision'; +$string['privacy:metadata:wiki_versions:content'] = 'Revision content'; +$string['privacy:metadata:wiki_versions:contentformat'] = 'Revision content format'; +$string['privacy:metadata:wiki_versions:version'] = 'Version number'; +$string['privacy:metadata:wiki_versions:timecreated'] = 'Time when revision was created'; $string['previewwarning'] = 'This is a preview. Changes have not been saved yet.'; $string['rated']='You rated this page as a {$a}'; $string['rating']='Rating'; diff --git a/mod/wiki/tests/generator/lib.php b/mod/wiki/tests/generator/lib.php index 401aebb608863..f99b249ac6ade 100644 --- a/mod/wiki/tests/generator/lib.php +++ b/mod/wiki/tests/generator/lib.php @@ -76,6 +76,42 @@ public function create_first_page($wiki, $record = array()) { return $this->create_page($wiki, $record); } + /** + * Retrieves or generates a subwiki and returns its id + * + * @param stdClass $wiki + * @param int $subwikiid + * @param int $group + * @param int $userid + * @return int + */ + public function get_subwiki($wiki, $subwikiid = null, $group = null, $userid = null) { + global $USER, $DB; + + if ($subwikiid) { + $params = ['id' => $subwikiid, 'wikiid' => $wiki->id]; + if ($group !== null) { + $params['group'] = $group; + } + if ($userid !== null) { + $params['userid'] = $userid; + } + return $DB->get_field('wiki_subwikis', 'id', $params, MUST_EXIST); + } + + if ($userid === null) { + $userid = ($wiki->wikimode == 'individual') ? $USER->id : 0; + } + if ($group === null) { + $group = 0; + } + if ($subwiki = wiki_get_subwiki_by_group($wiki->id, $group, $userid)) { + return $subwiki->id; + } else { + return wiki_add_subwiki($wiki->id, $group, $userid); + } + } + /** * Generates a page in wiki. * @@ -92,23 +128,15 @@ public function create_page($wiki, $record = array()) { 'title' => 'wiki page '.$this->pagecount, 'wikiid' => $wiki->id, 'subwikiid' => 0, - 'group' => 0, + 'group' => null, + 'userid' => null, 'content' => 'Wiki page content '.$this->pagecount, 'format' => $wiki->defaultformat ); if (empty($record['wikiid']) && empty($record['subwikiid'])) { throw new coding_exception('wiki page generator requires either wikiid or subwikiid'); } - if (!$record['subwikiid']) { - if (!isset($record['userid'])) { - $record['userid'] = ($wiki->wikimode == 'individual') ? $USER->id : 0; - } - if ($subwiki = wiki_get_subwiki_by_group($record['wikiid'], $record['group'], $record['userid'])) { - $record['subwikiid'] = $subwiki->id; - } else { - $record['subwikiid'] = wiki_add_subwiki($record['wikiid'], $record['group'], $record['userid']); - } - } + $record['subwikiid'] = $this->get_subwiki($wiki, $record['subwikiid'], $record['group'], $record['userid']); $wikipage = wiki_get_page_by_title($record['subwikiid'], $record['title']); if (!$wikipage) { diff --git a/mod/wiki/tests/privacy_test.php b/mod/wiki/tests/privacy_test.php new file mode 100644 index 0000000000000..7fa9293858150 --- /dev/null +++ b/mod/wiki/tests/privacy_test.php @@ -0,0 +1,532 @@ +. + +/** + * Data provider tests. + * + * @package mod_wiki + * @category test + * @copyright 2018 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); +global $CFG; + +use core_privacy\tests\provider_testcase; +use mod_wiki\privacy\provider; +use core_privacy\local\request\approved_contextlist; +use core_privacy\local\request\writer; + +require_once($CFG->dirroot.'/mod/wiki/locallib.php'); + +/** + * Data provider testcase class. + * + * @package mod_wiki + * @category test + * @copyright 2018 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_wiki_privacy_testcase extends provider_testcase { + + /** @var array */ + protected $users = []; + /** @var array */ + protected $pages = []; + /** @var array */ + protected $contexts = []; + /** @var array */ + protected $subwikis = []; + /** @var array */ + protected $pagepaths = []; + + /** + * Set up for each test. + * + * There are three users and four wikis. + * 1 : collaborative wiki, has context $this->contexts[1] and has a single subwiki $this->subwikis[1] + * 2 : individual wiki, has context $this->contexts[2] and three subwikis (one for each user): + * $this->subwikis[21], $this->subwikis[22], $this->subwikis[23], + * the subwiki for the third user is empty + * 3 : collaborative wiki, has context $this->contexts[3] and has a single subwiki $this->subwikis[3] + * 4 : collaborative wiki, has context $this->contexts[4], this wiki is empty + * + * Each subwiki (except for "23") has pages, for example, in $this->subwiki[1] there are pages + * $this->pages[1][1], $this->pages[1][2] and $this->pages[1][3] + * In the export data they have paths: + * $this->pagepaths[1][1], $this->pagepaths[1][2], $this->pagepaths[1][3] + */ + public function setUp() { + global $DB; + $this->resetAfterTest(); + + $dg = $this->getDataGenerator(); + $course = $dg->create_course(); + + $this->users[1] = $dg->create_user(); + $this->users[2] = $dg->create_user(); + $this->users[3] = $dg->create_user(); + + $studentrole = $DB->get_record('role', array('shortname' => 'student')); + $this->getDataGenerator()->enrol_user($this->users[1]->id, $course->id, $studentrole->id, 'manual'); + $this->getDataGenerator()->enrol_user($this->users[2]->id, $course->id, $studentrole->id, 'manual'); + $this->getDataGenerator()->enrol_user($this->users[3]->id, $course->id, $studentrole->id, 'manual'); + + $cm1 = $this->getDataGenerator()->create_module('wiki', ['course' => $course->id]); + $cm2 = $this->getDataGenerator()->create_module('wiki', ['course' => $course->id, 'wikimode' => 'individual']); + $cm3 = $this->getDataGenerator()->create_module('wiki', ['course' => $course->id]); + $cm4 = $this->getDataGenerator()->create_module('wiki', ['course' => $course->id]); // Empty. + + // User1. + $this->setUser($this->users[1]); + + // Create and modify pages in collaborative wiki. + $this->pages[1][1] = $this->create_first_page($cm1); + $this->pages[1][2] = $this->create_page($cm1, ['content' => 'initial content']); + $this->update_page($cm1, $this->pages[1][2], ['content' => 'update1 ']); + $this->attach_file($cm1, "Dog jump.jpg", 'jpg:Doggy'); + $this->update_page($cm1, $this->pages[1][2], ['content' => 'update2']); + + // Create pages in individual wiki, add files that are not used in text. + $this->pages[21][1] = $this->create_first_page($cm2); + $this->pages[21][2] = $this->create_page($cm2); + $this->attach_file($cm2, "mycat.jpg", 'jpg:Cat'); + + // User2. + $this->setUser($this->users[2]); + + // Modify existing pages in the first collaborative wiki. + $this->update_page($cm1, $this->pages[1][2], ['content' => 'update3 ']); + $this->attach_file($cm1, "Hamster.jpg", 'jpg:Hamster'); + + // Create pages in individual wiki. + $this->pages[22][1] = $this->create_first_page($cm2); + $this->pages[22][2] = $this->create_page($cm2); + + // Create pages in the third wiki. + $this->pages[3][1] = $this->create_first_page($cm3); + + // User3 (testing locks and empty subwiki). + $this->setUser($this->users[3]); + + // Create a subwiki in the individual wiki without any pages. + $subwiki23 = $dg->get_plugin_generator('mod_wiki')->get_subwiki($cm2); + + // Create a page in the first wiki and then lock it. + $this->pages[1][3] = $this->create_page($cm1); + wiki_set_lock($this->pages[1][3]->id, $this->users[3]->id, null, true); + + // Lock a page in the third wiki without having any revisions on it. + wiki_set_lock($this->pages[3][1]->id, $this->users[3]->id, null, true); + + $this->subwikis = [ + 1 => $this->pages[1][1]->subwikiid, + 21 => $this->pages[21][1]->subwikiid, + 22 => $this->pages[22][1]->subwikiid, + 23 => $subwiki23, + 3 => $this->pages[3][1]->subwikiid, + ]; + + $this->contexts = [ + 1 => context_module::instance($cm1->cmid), + 2 => context_module::instance($cm2->cmid), + 3 => context_module::instance($cm3->cmid), + 4 => context_module::instance($cm4->cmid), + ]; + + $this->pagepaths = [ + 1 => [ + 1 => $this->pages[1][1]->id . ' ' . $this->pages[1][1]->title, + 2 => $this->pages[1][2]->id . ' ' . $this->pages[1][2]->title, + 3 => $this->pages[1][3]->id . ' ' . $this->pages[1][3]->title, + ], + 21 => [ + 1 => $this->pages[21][1]->id . ' ' . $this->pages[21][1]->title, + 2 => $this->pages[21][2]->id . ' ' . $this->pages[21][2]->title, + ], + 22 => [ + 1 => $this->pages[22][1]->id . ' ' . $this->pages[22][1]->title, + 2 => $this->pages[22][2]->id . ' ' . $this->pages[22][2]->title, + ], + 3 => [ + 1 => $this->pages[3][1]->id . ' ' . $this->pages[3][1]->title, + ] + ]; + } + + /** + * Generate first page in wiki as current user + * + * @param stdClass $wiki + * @param array $record + * @return mixed + */ + protected function create_first_page($wiki, $record = []) { + $dg = $this->getDataGenerator(); + $wg = $dg->get_plugin_generator('mod_wiki'); + return $wg->create_first_page($wiki, $record); + } + + /** + * Generate a page in wiki as current user + * + * @param stdClass $wiki + * @param array $record + * @return mixed + */ + protected function create_page($wiki, $record = []) { + $dg = $this->getDataGenerator(); + $wg = $dg->get_plugin_generator('mod_wiki'); + return $wg->create_page($wiki, $record); + } + + /** + * Update an existing page in wiki as current user + * + * @param stdClass $wiki + * @param stdClass $page + * @param array $record + * @return mixed + */ + protected function update_page($wiki, $page, $record = []) { + $dg = $this->getDataGenerator(); + $wg = $dg->get_plugin_generator('mod_wiki'); + return $wg->create_page($wiki, ['title' => $page->title] + $record); + } + + /** + * Attach file to a wiki as a current user + * + * @param stdClass $wiki + * @param string $filename + * @param string $filecontent + * @return stored_file + */ + protected function attach_file($wiki, $filename, $filecontent) { + $dg = $this->getDataGenerator(); + $wg = $dg->get_plugin_generator('mod_wiki'); + $subwikiid = $wg->get_subwiki($wiki); + + $fs = get_file_storage(); + return $fs->create_file_from_string([ + 'contextid' => context_module::instance($wiki->cmid)->id, + 'component' => 'mod_wiki', + 'filearea' => 'attachments', + 'itemid' => $subwikiid, + 'filepath' => '/', + 'filename' => $filename, + ], $filecontent); + } + + /** + * Test getting the contexts for a user. + */ + public function test_get_contexts_for_userid() { + + // Get contexts for the first user. + $contextids = provider::get_contexts_for_userid($this->users[1]->id)->get_contextids(); + $this->assertEquals([ + $this->contexts[1]->id, + $this->contexts[2]->id, + ], $contextids, '', 0.0, 10, true); + + // Get contexts for the second user. + $contextids = provider::get_contexts_for_userid($this->users[2]->id)->get_contextids(); + $this->assertEquals([ + $this->contexts[1]->id, + $this->contexts[2]->id, + $this->contexts[3]->id, + ], $contextids, '', 0.0, 10, true); + + // Get contexts for the third user. + $contextids = provider::get_contexts_for_userid($this->users[3]->id)->get_contextids(); + $this->assertEquals([ + $this->contexts[1]->id, + $this->contexts[2]->id, + $this->contexts[3]->id, + ], $contextids, '', 0.0, 10, true); + } + + /** + * Export data for user 1 + */ + public function test_export_user_data1() { + + // Export all contexts for the first user. + $contextids = array_values(array_map(function($c) { + return $c->id; + }, $this->contexts)); + $appctx = new approved_contextlist($this->users[1], 'mod_wiki', $contextids); + provider::export_user_data($appctx); + + // First wiki has two pages ever touched by this user. + $data = writer::with_context($this->contexts[1])->get_related_data([$this->subwikis[1]]); + $this->assertEquals([ + $this->pagepaths[1][1], + $this->pagepaths[1][2] + ], array_keys($data)); + // First page was initially created by this user and all its information is returned to this user. + $data11 = $data[$this->pagepaths[1][1]]; + $this->assertEquals($this->pages[1][1]->cachedcontent, $data11['page']['cachedcontent']); + $this->assertNotEmpty($data11['page']['timecreated']); + // Wiki creates two revisions when page is created, first one with empty content. + $this->assertEquals(2, count($data11['revisions'])); + $this->assertFalse(array_key_exists('locks', $data11)); + // Only one file is returned that was in the revision made by this user. + + // The second page was last modified by a different user, so userid in the wiki_pages table is different, + // additional page information is not exported. + $data12 = $data[$this->pagepaths[1][2]]; + $this->assertFalse(isset($data12['page']['timecreated'])); + // There are two revisions for creating the page and two additional revisions made by this user. + $this->assertEquals(4, count($data12['revisions'])); + $lastrevision = array_pop($data12['revisions']); + $this->assertEquals('update2', $lastrevision['content']); + + // There is one file that was used in this user's contents - "Dog face.jpg" and one file in page cachedcontents. + $files = writer::with_context($this->contexts[1])->get_files([$this->subwikis[1]]); + $this->assertEquals(['Dog jump.jpg', 'Hamster.jpg'], array_keys($files), '', 0, 10, true); + + // Second (individual) wiki for the first user, two pages are returned for this user's subwiki. + $data = writer::with_context($this->contexts[2])->get_related_data([$this->subwikis[21]]); + $this->assertEquals([ + $this->pagepaths[21][1], + $this->pagepaths[21][2] + ], array_keys($data)); + $files = writer::with_context($this->contexts[2])->get_files([$this->subwikis[21]]); + $this->assertEquals(['mycat.jpg'], array_keys($files)); + + // Second (individual) wiki for the first user, nothing is returned for the second user's subwiki. + $this->assertFalse(writer::with_context($this->contexts[2])->has_any_data([$this->subwikis[22]])); + + // Third wiki for the first user, there were no contributions by the first user. + $this->assertFalse(writer::with_context($this->contexts[3])->has_any_data([$this->subwikis[3]])); + } + + /** + * Test export data for user 2 + */ + public function test_export_user_data2() { + + // Export all contexts for the second user. + $contextids = array_values(array_map(function($c) { + return $c->id; + }, $this->contexts)); + $appctx = new approved_contextlist($this->users[2], 'mod_wiki', $contextids); + provider::export_user_data($appctx); + + // First wiki - this user only modified the second page. + $data = writer::with_context($this->contexts[1])->get_related_data([$this->subwikis[1]]); + $this->assertEquals([ + $this->pagepaths[1][2] + ], array_keys($data)); + + // This user was the last one to modify this page, so the page info is returned. + $data12 = $data[$this->pagepaths[1][2]]; + $this->assertEquals('update3 Hamster.jpg', $data12['page']['cachedcontent']); + // He made one revision. + $this->assertEquals(1, count($data12['revisions'])); + $lastrevision = reset($data12['revisions']); + $this->assertEquals('update3 ', $lastrevision['content']); + + // Only one file was used in the first wiki by this user - Hamster.jpg. + $files = writer::with_context($this->contexts[1])->get_files([$this->subwikis[1]]); + $this->assertEquals(['Hamster.jpg'], array_keys($files)); + + // Export second (individual) wiki, nothing is returned for the other user's subwiki. + $this->assertFalse(writer::with_context($this->contexts[2])->has_any_data([$this->subwikis[21]])); + + // Export second (individual) wiki, two pages are returned for this user's subwiki. + $data = writer::with_context($this->contexts[2])->get_related_data([$this->subwikis[22]]); + $this->assertEquals([ + $this->pagepaths[22][1], + $this->pagepaths[22][2] + ], array_keys($data)); + $files = writer::with_context($this->contexts[2])->get_files([$this->subwikis[22]]); + $this->assertEmpty($files); + + // Second user made contributions to the third wiki. + $data = writer::with_context($this->contexts[3])->get_related_data([$this->subwikis[3]]); + $this->assertEquals([ + $this->pagepaths[3][1] + ], array_keys($data)); + $files = writer::with_context($this->contexts[3])->get_files([$this->subwikis[3]]); + $this->assertEmpty($files); + } + + /** + * Test export data for user 3 (locks, empty individual wiki) + */ + public function test_export_user_data3() { + + // Export all contexts for the third user. + $contextids = array_values(array_map(function($c) { + return $c->id; + }, $this->contexts)); + $appctx = new approved_contextlist($this->users[3], 'mod_wiki', $contextids); + provider::export_user_data($appctx); + + // For the third page of the first wiki there are 2 revisions and 1 lock. + $data = writer::with_context($this->contexts[1])->get_related_data([$this->subwikis[1]]); + $this->assertEquals([ + $this->pagepaths[1][3] + ], array_keys($data)); + + $data13 = $data[$this->pagepaths[1][3]]; + $this->assertNotEmpty($data13['page']['timecreated']); + $this->assertEquals(2, count($data13['revisions'])); + $this->assertEquals(1, count($data13['locks'])); + $files = writer::with_context($this->contexts[1])->get_files([$this->subwikis[1]]); + $this->assertEmpty($files); + + // Empty individual wiki. + $this->assertTrue(writer::with_context($this->contexts[2])->has_any_data()); + $data = writer::with_context($this->contexts[2])->get_data([$this->subwikis[23]]); + $this->assertEquals((object)[ + 'groupid' => 0, + 'userid' => $this->users[3]->id + ], $data); + $files = writer::with_context($this->contexts[2])->get_files([$this->subwikis[23]]); + $this->assertEmpty($files); + + // For the third wiki there is no page information, no revisions and one lock. + $data = writer::with_context($this->contexts[3])->get_related_data([$this->subwikis[3]]); + $this->assertEquals([ + $this->pagepaths[3][1] + ], array_keys($data)); + + $data31 = $data[$this->pagepaths[3][1]]; + $this->assertTrue(empty($data31['page']['timecreated'])); + $this->assertTrue(empty($data31['revisions'])); + $this->assertEquals(1, count($data31['locks'])); + + $files = writer::with_context($this->contexts[3])->get_files([$this->subwikis[3]]); + $this->assertEmpty($files); + + // No data for the forth wiki. + $this->assertFalse(writer::with_context($this->contexts[4])->has_any_data()); + } + + /** + * Creates a comment object + * + * @param stdClass $page + * @param string $text + * @return comment The comment object. + */ + protected function add_comment($page, $text) { + global $DB, $CFG, $USER; + require_once($CFG->dirroot . '/comment/lib.php'); + $record = $DB->get_record_sql('SELECT cm.id, cm.course FROM {course_modules} cm + JOIN {modules} m ON m.name = ? AND m.id = cm.module + JOIN {wiki} w ON cm.instance = w.id + JOIN {wiki_subwikis} s ON s.wikiid = w.id + WHERE s.id=?', ['wiki', $page->subwikiid]); + $context = context_module::instance($record->id); + $args = new stdClass; + $args->context = $context; + $args->courseid = $record->course; + $args->area = 'wiki_page'; + $args->itemid = $page->id; + $args->component = 'mod_wiki'; + $comment = new comment($args); + $comment->set_post_permission(true); + $comment->add($text); + return $comment; + } + + /** + * Test export data when there are comments. + */ + public function test_export_user_data_with_comments() { + global $DB; + // Comment on each page in the first wiki as the first user. + $this->setUser($this->users[1]); + $this->add_comment($this->pages[1][1], 'Hello111'); + $this->add_comment($this->pages[1][2], 'Hello112'); + $this->add_comment($this->pages[1][3], 'Hello113'); + + // Comment on second and third page as the third user. + $this->setUser($this->users[3]); + $this->add_comment($this->pages[1][2], 'Hello312'); + $this->add_comment($this->pages[1][3], 'Hello313'); + + // Export all contexts for the third user. + $contextids = array_values(array_map(function($c) { + return $c->id; + }, $this->contexts)); + $appctx = new approved_contextlist($this->users[3], 'mod_wiki', $contextids); + provider::export_user_data($appctx); + + $data = writer::with_context($this->contexts[1])->get_related_data([$this->subwikis[1]]); + // Now user has two pages (comparing to previous test where he had one). + $this->assertEquals([ + $this->pagepaths[1][2], + $this->pagepaths[1][3] + ], array_keys($data)); + + // Page 1-2 was exported and it has one comment that this user made (comment from another user was not exported). + $data12 = $data[$this->pagepaths[1][2]]; + $this->assertTrue(empty($data12['page']['timecreated'])); + $this->assertTrue(empty($data12['revisions'])); + $this->assertTrue(empty($data12['locks'])); + $this->assertEquals(1, count($data12['page']['comments'])); + + // Page 1-3 was exported same way as in the previous test and it has two comments. + $data13 = $data[$this->pagepaths[1][3]]; + $this->assertNotEmpty($data13['page']['timecreated']); + $this->assertEquals(2, count($data13['revisions'])); + $this->assertEquals(1, count($data13['locks'])); + $this->assertEquals(2, count($data13['page']['comments'])); + } + + /** + * Test for delete_data_for_all_users_in_context(). + */ + public function test_delete_data_for_all_users_in_context() { + provider::delete_data_for_all_users_in_context($this->contexts[1]); + + $appctx = new approved_contextlist($this->users[1], 'mod_wiki', + [$this->contexts[1]->id, $this->contexts[2]->id]); + provider::export_user_data($appctx); + $this->assertFalse(writer::with_context($this->contexts[1])->has_any_data()); + $this->assertTrue(writer::with_context($this->contexts[2])->has_any_data()); + + writer::reset(); + $appctx = new approved_contextlist($this->users[2], 'mod_wiki', [$this->contexts[1]->id]); + provider::export_user_data($appctx); + $this->assertFalse(writer::with_context($this->contexts[1])->has_any_data()); + + writer::reset(); + $appctx = new approved_contextlist($this->users[3], 'mod_wiki', [$this->contexts[1]->id]); + provider::export_user_data($appctx); + $this->assertFalse(writer::with_context($this->contexts[1])->has_any_data()); + } + + /** + * Test for delete_data_for_user(). + */ + public function test_delete_data_for_user() { + $appctx = new approved_contextlist($this->users[1], 'mod_wiki', + [$this->contexts[1]->id, $this->contexts[1]->id]); + provider::delete_data_for_user($appctx); + + provider::export_user_data($appctx); + $this->assertTrue(writer::with_context($this->contexts[1])->has_any_data()); + $this->assertFalse(writer::with_context($this->contexts[2])->has_any_data()); + } +} diff --git a/privacy/classes/tests/request/content_writer.php b/privacy/classes/tests/request/content_writer.php index 4531a2fbf6a8f..72fcfe2218a65 100644 --- a/privacy/classes/tests/request/content_writer.php +++ b/privacy/classes/tests/request/content_writer.php @@ -74,28 +74,41 @@ class content_writer implements \core_privacy\local\request\content_writer { /** * Whether any data has been exported at all within the current context. * + * @param array $subcontext The location within the current context that this data belongs - + * in this method it can be partial subcontext path (or none at all to check presence of any data anywhere). + * User preferences never have subcontext, if $subcontext is specified, user preferences are not checked. * @return bool */ - public function has_any_data() { - $hasdata = !empty($this->data->{$this->context->id}); - $hasrelateddata = !empty($this->relateddata->{$this->context->id}); - $hasmetadata = !empty($this->metadata->{$this->context->id}); - $hasfiles = !empty($this->files->{$this->context->id}); - $hascustomfiles = !empty($this->customfiles->{$this->context->id}); - $hasuserprefs = !empty($this->userprefs->{$this->context->id}); - - $systemcontext = \context_system::instance(); - $hasglobaluserprefs = !empty($this->userprefs->{$systemcontext->id}); - - $hasanydata = $hasdata; - $hasanydata = $hasanydata || $hasrelateddata; - $hasanydata = $hasanydata || $hasmetadata; - $hasanydata = $hasanydata || $hasfiles; - $hasanydata = $hasanydata || $hascustomfiles; - $hasanydata = $hasanydata || $hasuserprefs; - $hasanydata = $hasanydata || $hasglobaluserprefs; + public function has_any_data($subcontext = []) { + if (empty($subcontext)) { + // When subcontext is not specified check presence of user preferences in this context and in system context. + $hasuserprefs = !empty($this->userprefs->{$this->context->id}); + $systemcontext = \context_system::instance(); + $hasglobaluserprefs = !empty($this->userprefs->{$systemcontext->id}); + if ($hasuserprefs || $hasglobaluserprefs) { + return true; + } + } - return $hasanydata; + foreach (['data', 'relateddata', 'metadata', 'files', 'customfiles'] as $datatype) { + if (!property_exists($this->$datatype, $this->context->id)) { + // No data of this type for this context at all. Continue to the next data type. + continue; + } + $basepath = $this->$datatype->{$this->context->id}; + foreach ($subcontext as $subpath) { + if (!isset($basepath->children->$subpath)) { + // No data of this type is present for this path. Continue to the next data type. + continue 2; + } + $basepath = $basepath->children->$subpath; + } + if (!empty($basepath)) { + // Some data found for this type for this subcontext. + return true; + } + } + return false; } /** @@ -358,6 +371,8 @@ public function get_custom_file(array $subcontext = [], $filename = null) { * actual writer exports files so it can be reliably tested only in real writers such as * {@link core_privacy\local\request\moodle_content_writer}. * + * However we have to remove @@PLUGINFILE@@ since otherwise {@link format_text()} shows debugging messages + * * @param array $subcontext The location within the current context that this data belongs. * @param string $component The name of the component that the files belong to. * @param string $filearea The filearea within that component. @@ -366,7 +381,7 @@ public function get_custom_file(array $subcontext = [], $filename = null) { * @return string The processed string */ public function rewrite_pluginfile_urls(array $subcontext, $component, $filearea, $itemid, $text) { - return $text; + return str_replace('@@PLUGINFILE@@/', 'files/', $text); } /** diff --git a/privacy/tests/tests_content_writer_test.php b/privacy/tests/tests_content_writer_test.php index 3783ba2ebe3b6..0e5759a65f079 100644 --- a/privacy/tests/tests_content_writer_test.php +++ b/privacy/tests/tests_content_writer_test.php @@ -87,6 +87,9 @@ public function test_export_data_no_context_clash() { $writer->set_context($context); $data = $writer->get_data(['data']); $this->assertSame($dataa, $data); + $this->assertTrue($writer->has_any_data()); + $this->assertTrue($writer->has_any_data(['data'])); + $this->assertFalse($writer->has_any_data(['somepath'])); $writer->set_context($usercontext); $data = $writer->get_data(['data']); @@ -181,6 +184,9 @@ public function test_export_metadata_no_context_clash() { $this->assertEquals('value2', $metadata->value); $this->assertEquals('description2', $metadata->description); $this->assertEquals('value2', $writer->get_metadata(['metadata'], 'somekey', true)); + $this->assertTrue($writer->has_any_data()); + $this->assertTrue($writer->has_any_data(['metadata'])); + $this->assertFalse($writer->has_any_data(['somepath'])); } /** @@ -326,6 +332,8 @@ public function test_export_file_no_context_clash() { $files = $writer->get_files([]); $this->assertCount(1, $files); $this->assertEquals($fileb, $files['foo/foo.txt']); + $this->assertTrue($writer->has_any_data()); + $this->assertFalse($writer->has_any_data(['somepath'])); } /** @@ -375,6 +383,10 @@ public function test_export_related_data() { $data = $writer->get_related_data(['file', 'data'], 'file'); $this->assertEquals('data1', $data); + $this->assertTrue($writer->has_any_data()); + $this->assertTrue($writer->has_any_data(['file'])); + $this->assertTrue($writer->has_any_data(['file', 'data'])); + $this->assertFalse($writer->has_any_data(['somepath'])); } /** @@ -440,6 +452,9 @@ public function test_export_custom_file() { $this->assertEquals('Content 1', $files['file.txt']); $file = $writer->get_custom_file(['file.txt'], 'file.txt'); $this->assertEquals('Content 1', $file); + $this->assertTrue($writer->has_any_data()); + $this->assertTrue($writer->has_any_data(['file.txt'])); + $this->assertFalse($writer->has_any_data(['somepath'])); } /**