4bbc744a1db4de284a12830d19925c563c472a6e
[moodle.git] / blog / classes / privacy / provider.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * Data provider.
19  *
20  * @package    core_blog
21  * @copyright  2018 Frédéric Massart
22  * @author     Frédéric Massart <fred@branchup.tech>
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
26 namespace core_blog\privacy;
27 defined('MOODLE_INTERNAL') || die();
29 use blog_entry;
30 use context;
31 use context_helper;
32 use context_user;
33 use context_system;
34 use core_tag_tag;
35 use core_privacy\local\metadata\collection;
36 use core_privacy\local\request\approved_contextlist;
37 use core_privacy\local\request\transform;
38 use core_privacy\local\request\writer;
40 require_once($CFG->dirroot . '/blog/locallib.php');
42 /**
43  * Data provider class.
44  *
45  * @package    core_blog
46  * @copyright  2018 Frédéric Massart
47  * @author     Frédéric Massart <fred@branchup.tech>
48  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
49  */
50 class provider implements
51     \core_privacy\local\metadata\provider,
52     \core_privacy\local\request\subsystem\provider,
53     \core_privacy\local\request\core_userlist_provider {
55     /**
56      * Returns metadata.
57      *
58      * @param collection $collection The initialised collection to add items to.
59      * @return collection A listing of user data stored through this system.
60      */
61     public static function get_metadata(collection $collection) : collection {
63         $collection->add_database_table('post', [
64             'userid' => 'privacy:metadata:post:userid',
65             'subject' => 'privacy:metadata:post:subject',
66             'summary' => 'privacy:metadata:post:summary',
67             'uniquehash' => 'privacy:metadata:post:uniquehash',
68             'publishstate' => 'privacy:metadata:post:publishstate',
69             'created' => 'privacy:metadata:post:created',
70             'lastmodified' => 'privacy:metadata:post:lastmodified',
72             // The following columns are unused:
73             // coursemoduleid, courseid, moduleid, groupid, rating, usermodified.
74         ], 'privacy:metadata:post');
76         $collection->link_subsystem('core_comment', 'privacy:metadata:core_comments');
77         $collection->link_subsystem('core_files', 'privacy:metadata:core_files');
78         $collection->link_subsystem('core_tag', 'privacy:metadata:core_tag');
80         $collection->add_database_table('blog_external', [
81             'userid' => 'privacy:metadata:external:userid',
82             'name' => 'privacy:metadata:external:name',
83             'description' => 'privacy:metadata:external:description',
84             'url' => 'privacy:metadata:external:url',
85             'filtertags' => 'privacy:metadata:external:filtertags',
86             'timemodified' => 'privacy:metadata:external:timemodified',
87             'timefetched' => 'privacy:metadata:external:timefetched',
88         ], 'privacy:metadata:external');
90         // We do not report on blog_association because this is just context-related data.
92         return $collection;
93     }
95     /**
96      * Get the list of contexts that contain user information for the specified user.
97      *
98      * @param int $userid The user to search.
99      * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin.
100      */
101     public static function get_contexts_for_userid(int $userid) : \core_privacy\local\request\contextlist {
102         global $DB;
103         $contextlist = new \core_privacy\local\request\contextlist();
105         // There are at least one blog post.
106         if ($DB->record_exists_select('post', 'userid = :userid AND module IN (:blog, :blogext)', [
107                 'userid' => $userid, 'blog' => 'blog', 'blogext' => 'blog_external'])) {
108             $sql = "
109                 SELECT ctx.id
110                   FROM {context} ctx
111                  WHERE ctx.contextlevel = :ctxlevel
112                    AND ctx.instanceid = :ctxuserid";
113             $params = [
114                 'ctxlevel' => CONTEXT_USER,
115                 'ctxuserid' => $userid,
116             ];
117             $contextlist->add_from_sql($sql, $params);
119             // Add the associated context of the blog posts.
120             $sql = "
121                 SELECT DISTINCT ctx.id
122                   FROM {post} p
123                   JOIN {blog_association} ba
124                     ON ba.blogid = p.id
125                   JOIN {context} ctx
126                     ON ctx.id = ba.contextid
127                  WHERE p.userid = :userid";
128             $params = [
129                 'userid' => $userid,
130             ];
131             $contextlist->add_from_sql($sql, $params);
132         }
134         // If there is at least one external blog, we add the user context. This is done this
135         // way because we can't directly add context to a contextlist.
136         if ($DB->record_exists('blog_external', ['userid' => $userid])) {
137             $sql = "
138                 SELECT ctx.id
139                   FROM {context} ctx
140                  WHERE ctx.contextlevel = :ctxlevel
141                    AND ctx.instanceid = :ctxuserid";
142             $params = [
143                 'ctxlevel' => CONTEXT_USER,
144                 'ctxuserid' => $userid,
145             ];
146             $contextlist->add_from_sql($sql, $params);
147         }
149         // Include the user contexts in which the user comments.
150         $sql = "
151             SELECT DISTINCT ctx.id
152               FROM {context} ctx
153               JOIN {comments} c
154                 ON c.contextid = ctx.id
155              WHERE c.component = :component
156                AND c.commentarea = :commentarea
157                AND c.userid = :userid";
158         $params = [
159             'component' => 'blog',
160             'commentarea' => 'format_blog',
161             'userid' => $userid
162         ];
163         $contextlist->add_from_sql($sql, $params);
165         return $contextlist;
166     }
168     /**
169      * Get the list of users who have data within a context.
170      *
171      * @param \core_privacy\local\request\userlist $userlist The userlist containing the list of users who have
172      * data in this context/plugin combination.
173      */
174     public static function get_users_in_context(\core_privacy\local\request\userlist $userlist) {
175         global $DB;
176         $context = $userlist->get_context();
177         if ($context->contextlevel == CONTEXT_COURSE || $context->contextlevel == CONTEXT_MODULE) {
179             $params = ['contextid' => $context->id];
181             $sql = "SELECT p.id, p.userid
182                       FROM {post} p
183                       JOIN {blog_association} ba ON ba.blogid = p.id AND ba.contextid = :contextid";
185             $posts = $DB->get_records_sql($sql, $params);
186             $userids = array_map(function($post) {
187                 return $post->userid;
188             }, $posts);
189             $userlist->add_users($userids);
191             if (!empty($posts)) {
192                 // Add any user's who posted on the blog.
193                 list($insql, $inparams) = $DB->get_in_or_equal(array_keys($posts), SQL_PARAMS_NAMED);
194                 \core_comment\privacy\provider::get_users_in_context_from_sql($userlist, 'c', 'blog', 'format_blog', null, $insql,
195                     $inparams);
196             }
197         } else if ($context->contextlevel == CONTEXT_USER) {
198             $params = ['userid' => $context->instanceid];
200             $sql = "SELECT userid
201                       FROM {blog_external}
202                      WHERE userid = :userid";
203             $userlist->add_from_sql('userid', $sql, $params);
205             $sql = "SELECT userid
206                       FROM {post}
207                      WHERE userid = :userid";
208             $userlist->add_from_sql('userid', $sql, $params);
210             // Add any user's who posted on the blog.
211             \core_comment\privacy\provider::get_users_in_context_from_sql($userlist, 'c', 'blog', 'format_blog', $context->id);
212         }
213     }
215     /**
216      * Export all user data for the specified user, in the specified contexts.
217      *
218      * @param approved_contextlist $contextlist The approved contexts to export information for.
219      */
220     public static function export_user_data(approved_contextlist $contextlist) {
221         global $DB;
223         $sysctx = context_system::instance();
224         $fs = get_file_storage();
225         $userid = $contextlist->get_user()->id;
226         $ctxfields = context_helper::get_preload_record_columns_sql('ctx');
227         $rootpath = [get_string('blog', 'core_blog')];
228         $associations = [];
230         foreach ($contextlist as $context) {
231             switch ($context->contextlevel) {
232                 case CONTEXT_USER:
233                     $contextuserid = $context->instanceid;
234                     $insql = ' > 0';
235                     $inparams = [];
237                     if ($contextuserid != $userid) {
238                         // We will only be exporting comments, so fetch the IDs of the relevant entries.
239                         $entryids = $DB->get_fieldset_sql("
240                             SELECT DISTINCT c.itemid
241                               FROM {comments} c
242                              WHERE c.contextid = :contextid
243                                AND c.userid = :userid
244                                AND c.component = :component
245                                AND c.commentarea = :commentarea", [
246                             'contextid' => $context->id,
247                             'userid' => $userid,
248                             'component' => 'blog',
249                             'commentarea' => 'format_blog'
250                         ]);
252                         if (empty($entryids)) {
253                             // This should not happen, as the user context should not have been reported then.
254                             continue 2;
255                         }
257                         list($insql, $inparams) = $DB->get_in_or_equal($entryids, SQL_PARAMS_NAMED);
258                     }
260                     // Loop over each blog entry in context.
261                     $sql = "userid = :userid AND module IN (:blog, :blogext) AND id $insql";
262                     $params = array_merge($inparams, ['userid' => $contextuserid, 'blog' => 'blog', 'blogext' => 'blog_external']);
263                     $recordset = $DB->get_recordset_select('post', $sql, $params, 'id');
264                     foreach ($recordset as $record) {
266                         $subject = format_string($record->subject);
267                         $path = array_merge($rootpath, [get_string('blogentries', 'core_blog'), $subject . " ({$record->id})"]);
269                         // If the context is not mine, then we ONLY export the comments made by the exporting user.
270                         if ($contextuserid != $userid) {
271                             \core_comment\privacy\provider::export_comments($context, 'blog', 'format_blog',
272                                 $record->id, $path, true);
273                             continue;
274                         }
276                         // Manually export the files as they reside in the system context so we can't use
277                         // the write's helper methods. The same happens for attachments.
278                         foreach ($fs->get_area_files($sysctx->id, 'blog', 'post', $record->id) as $f) {
279                             writer::with_context($context)->export_file($path, $f);
280                         }
281                         foreach ($fs->get_area_files($sysctx->id, 'blog', 'attachment', $record->id) as $f) {
282                             writer::with_context($context)->export_file($path, $f);
283                         }
285                         // Rewrite the summary files.
286                         $summary = writer::with_context($context)->rewrite_pluginfile_urls($path, 'blog', 'post',
287                             $record->id, $record->summary);
289                         // Fetch associations.
290                         $assocs = [];
291                         $sql = "SELECT ba.contextid, $ctxfields
292                                   FROM {blog_association} ba
293                                   JOIN {context} ctx
294                                     ON ba.contextid = ctx.id
295                                  WHERE ba.blogid = :blogid";
296                         $assocset = $DB->get_recordset_sql($sql, ['blogid' => $record->id]);
297                         foreach ($assocset as $assocrec) {
298                             context_helper::preload_from_record($assocrec);
299                             $assocctx = context::instance_by_id($assocrec->contextid);
300                             $assocs[] = $assocctx->get_context_name();
301                         }
302                         $assocset->close();
304                         // Export associated tags.
305                         \core_tag\privacy\provider::export_item_tags($userid, $context, $path, 'core', 'post', $record->id);
307                         // Export all comments made on my post.
308                         \core_comment\privacy\provider::export_comments($context, 'blog', 'format_blog',
309                             $record->id, $path, false);
311                         // Add blog entry data.
312                         $entry = (object) [
313                             'subject' => $subject,
314                             'summary' => format_text($summary, $record->summaryformat),
315                             'uniquehash' => $record->uniquehash,
316                             'publishstate' => static::transform_publishstate($record->publishstate),
317                             'created' => transform::datetime($record->created),
318                             'lastmodified' => transform::datetime($record->lastmodified),
319                             'associations' => $assocs
320                         ];
322                         writer::with_context($context)->export_data($path, $entry);
323                     }
324                     $recordset->close();
326                     // Export external blogs.
327                     $recordset = $DB->get_recordset('blog_external', ['userid' => $userid]);
328                     foreach ($recordset as $record) {
330                         $path = array_merge($rootpath, [get_string('externalblogs', 'core_blog'),
331                             $record->name . " ({$record->id})"]);
333                         // Export associated tags.
334                         \core_tag\privacy\provider::export_item_tags($userid, $context, $path, 'core',
335                             'blog_external', $record->id);
337                         // Add data.
338                         $external = (object) [
339                             'name' => $record->name,
340                             'description' => $record->description,
341                             'url' => $record->url,
342                             'filtertags' => $record->filtertags,
343                             'modified' => transform::datetime($record->timemodified),
344                             'lastfetched' => transform::datetime($record->timefetched),
345                         ];
347                         writer::with_context($context)->export_data($path, $external);
348                     }
349                     $recordset->close();
350                     break;
352                 case CONTEXT_COURSE:
353                 case CONTEXT_MODULE:
354                     $associations[] = $context->id;
355                     break;
356             }
357         }
359         // Export associations.
360         if (!empty($associations)) {
361             list($insql, $inparams) = $DB->get_in_or_equal($associations, SQL_PARAMS_NAMED);
362             $sql = "
363                 SELECT ba.contextid, p.subject, $ctxfields
364                   FROM {post} p
365                   JOIN {blog_association} ba
366                     ON ba.blogid = p.id
367                   JOIN {context} ctx
368                     ON ctx.id = ba.contextid
369                  WHERE ba.contextid $insql
370                    AND p.userid = :userid
371               ORDER BY ba.contextid ASC";
372             $params = array_merge($inparams, ['userid' => $userid]);
374             $path = [get_string('privacy:path:blogassociations', 'core_blog')];
376             $flushassocs = function($context, $assocs) use ($path) {
377                 writer::with_context($context)->export_data($path, (object) [
378                     'associations' => $assocs
379                 ]);
380             };
382             $lastcontextid = null;
383             $assocs = [];
384             $recordset = $DB->get_recordset_sql($sql, $params);
385             foreach ($recordset as $record) {
386                 context_helper::preload_from_record($record);
388                 if ($lastcontextid && $record->contextid != $lastcontextid) {
389                     $flushassocs(context::instance_by_id($lastcontextid), $assocs);
390                     $assocs = [];
391                 }
392                 $assocs[] = format_string($record->subject);
393                 $lastcontextid = $record->contextid;
394             }
396             if ($lastcontextid) {
397                 $flushassocs(context::instance_by_id($lastcontextid), $assocs);
398             }
400             $recordset->close();
401         }
402     }
404     /**
405      * Delete all data for all users in the specified context.
406      *
407      * @param context $context The specific context to delete data for.
408      */
409     public static function delete_data_for_all_users_in_context(context $context) {
410         global $DB;
411         switch ($context->contextlevel) {
412             case CONTEXT_USER:
413                 static::delete_all_user_data($context);
414                 break;
416             case CONTEXT_COURSE:
417             case CONTEXT_MODULE:
418                 // We only delete associations here.
419                 $DB->delete_records('blog_association', ['contextid' => $context->id]);
420                 break;
421         }
422         // Delete all the comments.
423         \core_comment\privacy\provider::delete_comments_for_all_users($context, 'blog', 'format_blog');
424     }
426     /**
427      * Delete all user data for the specified user, in the specified contexts.
428      *
429      * @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
430      */
431     public static function delete_data_for_user(approved_contextlist $contextlist) {
432         global $DB;
433         $userid = $contextlist->get_user()->id;
434         $associationcontextids = [];
436         foreach ($contextlist as $context) {
437             if ($context->contextlevel == CONTEXT_USER && $context->instanceid == $userid) {
438                 static::delete_all_user_data($context);
439                 \core_comment\privacy\provider::delete_comments_for_all_users($context, 'blog', 'format_blog');
440             } else if ($context->contextlevel == CONTEXT_COURSE) {
441                 // Only delete the course associations.
442                 $associationcontextids[] = $context->id;
443             } else if ($context->contextlevel == CONTEXT_MODULE) {
444                 // Only delete the module associations.
445                 $associationcontextids[] = $context->id;
446             } else {
447                 \core_comment\privacy\provider::delete_comments_for_user($contextlist, 'blog', 'format_blog');
448             }
449         }
451         // Delete the associations.
452         if (!empty($associationcontextids)) {
453             list($insql, $inparams) = $DB->get_in_or_equal($associationcontextids, SQL_PARAMS_NAMED);
454             $sql = "SELECT ba.id
455                       FROM {blog_association} ba
456                       JOIN {post} p
457                         ON p.id = ba.blogid
458                      WHERE ba.contextid $insql
459                        AND p.userid = :userid";
460             $params = array_merge($inparams, ['userid' => $userid]);
461             $associds = $DB->get_fieldset_sql($sql, $params);
463             list($insql, $inparams) = $DB->get_in_or_equal($associds, SQL_PARAMS_NAMED, 'param', true);
464             $DB->delete_records_select('blog_association', "id $insql", $inparams);
465         }
466     }
468     /**
469      * Delete multiple users within a single context.
470      *
471      * @param   approved_userlist       $userlist The approved context and user information to delete information for.
472      */
473     public static function delete_data_for_users(\core_privacy\local\request\approved_userlist $userlist) {
474         global $DB;
476         $context = $userlist->get_context();
477         $userids = $userlist->get_userids();
479         if ($context->contextlevel == CONTEXT_USER) {
480             // If one of the listed users matches this context then delete the blog, associations, and comments.
481             if (array_search($context->instanceid, $userids) !== false) {
482                 self::delete_all_user_data($context);
483                 \core_comment\privacy\provider::delete_comments_for_all_users($context, 'blog', 'format_blog');
484                 return;
485             }
486             \core_comment\privacy\provider::delete_comments_for_users($userlist, 'blog', 'format_blog');
487         } else {
488             list($insql, $inparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
489             $sql = "SELECT ba.id
490                       FROM {blog_association} ba
491                       JOIN {post} p ON p.id = ba.blogid
492                      WHERE ba.contextid = :contextid
493                        AND p.userid $insql";
494             $inparams['contextid'] = $context->id;
495             $associds = $DB->get_fieldset_sql($sql, $inparams);
497             if (!empty($associds)) {
498                 list($insql, $inparams) = $DB->get_in_or_equal($associds, SQL_PARAMS_NAMED, 'param', true);
499                 $DB->delete_records_select('blog_association', "id $insql", $inparams);
500             }
501         }
502     }
504     /**
505      * Helper method to delete all user data.
506      *
507      * @param context_user $usercontext The user context.
508      * @return void
509      */
510     protected static function delete_all_user_data(context_user $usercontext) {
511         global $DB;
512         $userid = $usercontext->instanceid;
514         // Delete all blog posts.
515         $recordset = $DB->get_recordset_select('post', 'userid = :userid AND module IN (:blog, :blogext)', [
516             'userid' => $userid, 'blog' => 'blog', 'blogext' => 'blog_external']);
517         foreach ($recordset as $record) {
518             $entry = new blog_entry(null, $record);
519             $entry->delete();   // Takes care of files and associations.
520         }
521         $recordset->close();
523         // Delete all external blogs, and their associated tags.
524         $DB->delete_records('blog_external', ['userid' => $userid]);
525         core_tag_tag::delete_instances('core', 'blog_external', $usercontext->id);
526     }
528     /**
529      * Transform a publish state.
530      *
531      * @param string $publishstate The publish state.
532      * @return string
533      */
534     public static function transform_publishstate($publishstate) {
535         switch ($publishstate) {
536             case 'draft':
537                 return get_string('publishtonoone', 'core_blog');
538             case 'site':
539                 return get_string('publishtosite', 'core_blog');
540             case 'public':
541                 return get_string('publishtoworld', 'core_blog');
542             default:
543         }
544         return get_string('privacy:unknown', 'core_blog');
545     }