MDL-62035 core_blog: Update to blog with comments.
[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 {
54     /**
55      * Returns metadata.
56      *
57      * @param collection $collection The initialised collection to add items to.
58      * @return collection A listing of user data stored through this system.
59      */
60     public static function get_metadata(collection $collection) : collection {
62         $collection->add_database_table('post', [
63             'userid' => 'privacy:metadata:post:userid',
64             'subject' => 'privacy:metadata:post:subject',
65             'summary' => 'privacy:metadata:post:summary',
66             'uniquehash' => 'privacy:metadata:post:uniquehash',
67             'publishstate' => 'privacy:metadata:post:publishstate',
68             'created' => 'privacy:metadata:post:created',
69             'lastmodified' => 'privacy:metadata:post:lastmodified',
71             // The following columns are unused:
72             // coursemoduleid, courseid, moduleid, groupid, rating, usermodified.
73         ], 'privacy:metadata:post');
75         $collection->link_subsystem('core_comments', 'privacy:metadata:core_comments');
76         $collection->link_subsystem('core_files', 'privacy:metadata:core_files');
77         $collection->link_subsystem('core_tag', 'privacy:metadata:core_tag');
79         $collection->add_database_table('blog_external', [
80             'userid' => 'privacy:metadata:external:userid',
81             'name' => 'privacy:metadata:external:name',
82             'description' => 'privacy:metadata:external:description',
83             'url' => 'privacy:metadata:external:url',
84             'filtertags' => 'privacy:metadata:external:filtertags',
85             'timemodified' => 'privacy:metadata:external:timemodified',
86             'timefetched' => 'privacy:metadata:external:timefetched',
87         ], 'privacy:metadata:external');
89         // We do not report on blog_association because this is just context-related data.
91         return $collection;
92     }
94     /**
95      * Get the list of contexts that contain user information for the specified user.
96      *
97      * @param int $userid The user to search.
98      * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin.
99      */
100     public static function get_contexts_for_userid(int $userid) : \core_privacy\local\request\contextlist {
101         global $DB;
102         $contextlist = new \core_privacy\local\request\contextlist();
104         // There are at least one blog post.
105         if ($DB->record_exists_select('post', 'userid = :userid AND module IN (:blog, :blogext)', [
106                 'userid' => $userid, 'blog' => 'blog', 'blogext' => 'blog_external'])) {
107             $sql = "
108                 SELECT ctx.id
109                   FROM {context} ctx
110                  WHERE ctx.contextlevel = :ctxlevel
111                    AND ctx.instanceid = :ctxuserid";
112             $params = [
113                 'ctxlevel' => CONTEXT_USER,
114                 'ctxuserid' => $userid,
115             ];
116             $contextlist->add_from_sql($sql, $params);
118             // Add the associated context of the blog posts.
119             $sql = "
120                 SELECT DISTINCT ctx.id
121                   FROM {post} p
122                   JOIN {blog_association} ba
123                     ON ba.blogid = p.id
124                   JOIN {context} ctx
125                     ON ctx.id = ba.contextid
126                  WHERE p.userid = :userid";
127             $params = [
128                 'userid' => $userid,
129             ];
130             $contextlist->add_from_sql($sql, $params);
131         }
133         // If there is at least one external blog, we add the user context. This is done this
134         // way because we can't directly add context to a contextlist.
135         if ($DB->record_exists('blog_external', ['userid' => $userid])) {
136             $sql = "
137                 SELECT ctx.id
138                   FROM {context}
139                  WHERE ctx.contextlevel = :ctxlevel
140                    AND ctx.instanceid = :ctxuserid";
141             $params = [
142                 'ctxlevel' => CONTEXT_USER,
143                 'ctxuserid' => $userid,
144             ];
145             $contextlist->add_from_sql($sql, $params);
146         }
148         // Include the user contexts in which the user comments.
149         $sql = "
150             SELECT DISTINCT ctx.id
151               FROM {context} ctx
152               JOIN {comments} c
153                 ON c.contextid = ctx.id
154              WHERE c.component = :component
155                AND c.commentarea = :commentarea
156                AND c.userid = :userid";
157         $params = [
158             'component' => 'blog',
159             'commentarea' => 'format_blog',
160             'userid' => $userid
161         ];
162         $contextlist->add_from_sql($sql, $params);
164         return $contextlist;
165     }
167     /**
168      * Export all user data for the specified user, in the specified contexts.
169      *
170      * @param approved_contextlist $contextlist The approved contexts to export information for.
171      */
172     public static function export_user_data(approved_contextlist $contextlist) {
173         global $DB;
175         $sysctx = context_system::instance();
176         $fs = get_file_storage();
177         $userid = $contextlist->get_user()->id;
178         $ctxfields = context_helper::get_preload_record_columns_sql('ctx');
179         $rootpath = [get_string('blog', 'core_blog')];
180         $associations = [];
182         foreach ($contextlist as $context) {
183             switch ($context->contextlevel) {
184                 case CONTEXT_USER:
185                     $contextuserid = $context->instanceid;
186                     $insql = ' > 0';
187                     $inparams = [];
189                     if ($contextuserid != $userid) {
190                         // We will only be exporting comments, so fetch the IDs of the relevant entries.
191                         $entryids = $DB->get_fieldset_sql("
192                             SELECT DISTINCT c.itemid
193                               FROM {comments} c
194                              WHERE c.contextid = :contextid
195                                AND c.userid = :userid
196                                AND c.component = :component
197                                AND c.commentarea = :commentarea", [
198                             'contextid' => $context->id,
199                             'userid' => $userid,
200                             'component' => 'blog',
201                             'commentarea' => 'format_blog'
202                         ]);
204                         if (empty($entryids)) {
205                             // This should not happen, as the user context should not have been reported then.
206                             continue;
207                         }
209                         list($insql, $inparams) = $DB->get_in_or_equal($entryids, SQL_PARAMS_NAMED);
210                     }
212                     // Loop over each blog entry in context.
213                     $sql = "userid = :userid AND module IN (:blog, :blogext) AND id $insql";
214                     $params = array_merge($inparams, ['userid' => $contextuserid, 'blog' => 'blog', 'blogext' => 'blog_external']);
215                     $recordset = $DB->get_recordset_select('post', $sql, $params, 'id');
216                     foreach ($recordset as $record) {
218                         $subject = format_string($record->subject);
219                         $path = array_merge($rootpath, [get_string('blogentries', 'core_blog'), $subject . " ({$record->id})"]);
221                         // If the context is not mine, then we ONLY export the comments made by the exporting user.
222                         if ($contextuserid != $userid) {
223                             \core_comment\privacy\provider::export_comments($context, 'blog', 'format_blog',
224                                 $record->id, $path, true);
225                             continue;
226                         }
228                         // Manually export the files as they reside in the system context so we can't use
229                         // the write's helper methods. The same happens for attachments.
230                         foreach ($fs->get_area_files($sysctx->id, 'blog', 'post', $record->id) as $f) {
231                             writer::with_context($context)->export_file($path, $f);
232                         }
233                         foreach ($fs->get_area_files($sysctx->id, 'blog', 'attachment', $record->id) as $f) {
234                             writer::with_context($context)->export_file($path, $f);
235                         }
237                         // Rewrite the summary files.
238                         $summary = writer::with_context($context)->rewrite_pluginfile_urls($path, 'blog', 'post',
239                             $record->id, $record->summary);
241                         // Fetch associations.
242                         $assocs = [];
243                         $sql = "SELECT ba.contextid, $ctxfields
244                                   FROM {blog_association} ba
245                                   JOIN {context} ctx
246                                     ON ba.contextid = ctx.id
247                                  WHERE ba.blogid = :blogid";
248                         $assocset = $DB->get_recordset_sql($sql, ['blogid' => $record->id]);
249                         foreach ($assocset as $assocrec) {
250                             context_helper::preload_from_record($assocrec);
251                             $assocctx = context::instance_by_id($assocrec->contextid);
252                             $assocs[] = $assocctx->get_context_name();
253                         }
254                         $assocset->close();
256                         // Export associated tags.
257                         \core_tag\privacy\provider::export_item_tags($userid, $context, $path, 'core', 'post', $record->id);
259                         // Export all comments made on my post.
260                         \core_comment\privacy\provider::export_comments($context, 'blog', 'format_blog',
261                             $record->id, $path, false);
263                         // Add blog entry data.
264                         $entry = (object) [
265                             'subject' => $subject,
266                             'summary' => format_text($summary, $record->summaryformat),
267                             'uniquehash' => $record->uniquehash,
268                             'publishstate' => static::transform_publishstate($record->publishstate),
269                             'created' => transform::datetime($record->created),
270                             'lastmodified' => transform::datetime($record->lastmodified),
271                             'associations' => $assocs
272                         ];
274                         writer::with_context($context)->export_data($path, $entry);
275                     }
276                     $recordset->close();
278                     // Export external blogs.
279                     $recordset = $DB->get_recordset('blog_external', ['userid' => $userid]);
280                     foreach ($recordset as $record) {
282                         $path = array_merge($rootpath, [get_string('externalblogs', 'core_blog'),
283                             $record->name . " ({$record->id})"]);
285                         // Export associated tags.
286                         \core_tag\privacy\provider::export_item_tags($userid, $context, $path, 'core',
287                             'blog_external', $record->id);
289                         // Add data.
290                         $external = (object) [
291                             'name' => $record->name,
292                             'description' => $record->description,
293                             'url' => $record->url,
294                             'filtertags' => $record->filtertags,
295                             'modified' => transform::datetime($record->timemodified),
296                             'lastfetched' => transform::datetime($record->timefetched),
297                         ];
299                         writer::with_context($context)->export_data($path, $external);
300                     }
301                     $recordset->close();
302                     break;
304                 case CONTEXT_COURSE:
305                 case CONTEXT_MODULE:
306                     $associations[] = $context->id;
307                     break;
308             }
309         }
311         // Export associations.
312         if (!empty($associations)) {
313             list($insql, $inparams) = $DB->get_in_or_equal($associations, SQL_PARAMS_NAMED);
314             $sql = "
315                 SELECT ba.contextid, p.subject, $ctxfields
316                   FROM {post} p
317                   JOIN {blog_association} ba
318                     ON ba.blogid = p.id
319                   JOIN {context} ctx
320                     ON ctx.id = ba.contextid
321                  WHERE ba.contextid $insql
322                    AND p.userid = :userid
323               ORDER BY ba.contextid ASC";
324             $params = array_merge($inparams, ['userid' => $userid]);
326             $path = [get_string('privacy:path:blogassociations', 'core_blog')];
328             $flushassocs = function($context, $assocs) use ($path) {
329                 writer::with_context($context)->export_data($path, (object) [
330                     'associations' => $assocs
331                 ]);
332             };
334             $lastcontextid = null;
335             $assocs = [];
336             $recordset = $DB->get_recordset_sql($sql, $params);
337             foreach ($recordset as $record) {
338                 context_helper::preload_from_record($record);
340                 if ($lastcontextid && $record->contextid != $lastcontextid) {
341                     $flushassocs(context::instance_by_id($lastcontextid), $assocs);
342                     $assocs = [];
343                 }
344                 $assocs[] = format_string($record->subject);
345                 $lastcontextid = $record->contextid;
346             }
348             if ($lastcontextid) {
349                 $flushassocs(context::instance_by_id($lastcontextid), $assocs);
350             }
352             $recordset->close();
353         }
354     }
356     /**
357      * Delete all data for all users in the specified context.
358      *
359      * @param context $context The specific context to delete data for.
360      */
361     public static function delete_data_for_all_users_in_context(context $context) {
362         global $DB;
363         switch ($context->contextlevel) {
364             case CONTEXT_USER:
365                 static::delete_all_user_data($context);
366                 break;
368             case CONTEXT_COURSE:
369             case CONTEXT_MODULE:
370                 // We only delete associations here.
371                 $DB->delete_records('blog_association', ['contextid' => $context->id]);
372                 break;
373         }
374         // Delete all the comments.
375         \core_comment\privacy\provider::delete_comments_for_all_users($context, 'blog', 'format_blog');
376     }
378     /**
379      * Delete all user data for the specified user, in the specified contexts.
380      *
381      * @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
382      */
383     public static function delete_data_for_user(approved_contextlist $contextlist) {
384         global $DB;
385         $userid = $contextlist->get_user()->id;
386         $associationcontextids = [];
388         foreach ($contextlist as $context) {
389             if ($context->contextlevel == CONTEXT_USER && $context->instanceid == $userid) {
390                 static::delete_all_user_data($context);
391                 \core_comment\privacy\provider::delete_comments_for_all_users($context, 'blog', 'format_blog');
392             } else if ($context->contextlevel == CONTEXT_COURSE) {
393                 // Only delete the course associations.
394                 $associationcontextids[] = $context->id;
395             } else if ($context->contextlevel == CONTEXT_MODULE) {
396                 // Only delete the module associations.
397                 $associationcontextids[] = $context->id;
398             } else {
399                 \core_comment\privacy\provider::delete_comments_for_user($contextlist, 'blog', 'format_blog');
400             }
401         }
403         // Delete the associations.
404         if (!empty($associationcontextids)) {
405             list($insql, $inparams) = $DB->get_in_or_equal($associationcontextids, SQL_PARAMS_NAMED);
406             $sql = "SELECT ba.id
407                       FROM {blog_association} ba
408                       JOIN {post} p
409                         ON p.id = ba.blogid
410                      WHERE ba.contextid $insql
411                        AND p.userid = :userid";
412             $params = array_merge($inparams, ['userid' => $userid]);
413             $associds = $DB->get_fieldset_sql($sql, $params);
415             list($insql, $inparams) = $DB->get_in_or_equal($associds, SQL_PARAMS_NAMED, 'param', true);
416             $DB->delete_records_select('blog_association', "id $insql", $inparams);
417         }
418     }
420     /**
421      * Helper method to delete all user data.
422      *
423      * @param context_user $usercontext The user context.
424      * @return void
425      */
426     protected static function delete_all_user_data(context_user $usercontext) {
427         global $DB;
428         $userid = $usercontext->instanceid;
430         // Delete all blog posts.
431         $recordset = $DB->get_recordset_select('post', 'userid = :userid AND module IN (:blog, :blogext)', [
432             'userid' => $userid, 'blog' => 'blog', 'blogext' => 'blog_external']);
433         foreach ($recordset as $record) {
434             $entry = new blog_entry(null, $record);
435             $entry->delete();   // Takes care of files and associations.
436         }
437         $recordset->close();
439         // Delete all external blogs, and their associated tags.
440         $DB->delete_records('blog_external', ['userid' => $userid]);
441         core_tag_tag::delete_instances('core', 'blog_external', $usercontext->id);
442     }
444     /**
445      * Transform a publish state.
446      *
447      * @param string $publishstate The publish state.
448      * @return string
449      */
450     public static function transform_publishstate($publishstate) {
451         switch ($publishstate) {
452             case 'draft':
453                 return get_string('publishtonoone', 'core_blog');
454             case 'site':
455                 return get_string('publishtosite', 'core_blog');
456             case 'public':
457                 return get_string('publishtoworld', 'core_blog');
458             default:
459         }
460         return get_string('privacy:unknown', 'core_blog');
461     }