Merge branch 'wip-MDL-61976-master' of git://github.com/marinaglancy/moodle

This commit is contained in:
David Monllao 2018-05-09 11:43:25 +02:00 committed by Jun Pataleta
commit 6fe7510ae0
6 changed files with 1140 additions and 31 deletions

View File

@ -0,0 +1,494 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* 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 {
$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(int $userid) : contextlist {
$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');
}
}

View File

@ -199,6 +199,31 @@ $string['pluginadministration'] = 'Wiki administration';
$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';

View File

@ -76,6 +76,42 @@ class mod_wiki_generator extends testing_module_generator {
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 @@ class mod_wiki_generator extends testing_module_generator {
'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) {

View File

@ -0,0 +1,532 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* 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 <img src="@@PLUGINFILE@@/Dog%20jump.jpg">']);
$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 <img src="@@PLUGINFILE@@/Hamster.jpg">']);
$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 <img src="files/Hamster.jpg" alt="Hamster.jpg" />', $data12['page']['cachedcontent']);
// He made one revision.
$this->assertEquals(1, count($data12['revisions']));
$lastrevision = reset($data12['revisions']);
$this->assertEquals('update3 <img src="files/Hamster.jpg">', $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());
}
}

View File

@ -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});
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;
}
}
$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;
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;
}
/**
@ -359,6 +372,8 @@ class content_writer implements \core_privacy\local\request\content_writer {
* 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.
@ -367,7 +382,7 @@ class content_writer implements \core_privacy\local\request\content_writer {
* @return string The processed string
*/
public function rewrite_pluginfile_urls(array $subcontext, $component, $filearea, $itemid, $text) : string {
return $text;
return str_replace('@@PLUGINFILE@@/', 'files/', $text);
}
/**

View File

@ -87,6 +87,9 @@ class tests_content_writer_test extends advanced_testcase {
$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 @@ class tests_content_writer_test extends advanced_testcase {
$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 @@ class tests_content_writer_test extends advanced_testcase {
$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 @@ class tests_content_writer_test extends advanced_testcase {
$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 @@ class tests_content_writer_test extends advanced_testcase {
$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']));
}
/**