Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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/>.
16
 
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
 */
25
 
26
namespace core_blog\privacy;
27
defined('MOODLE_INTERNAL') || die();
28
 
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;
39
 
40
require_once($CFG->dirroot . '/blog/locallib.php');
41
 
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 {
54
 
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 {
62
 
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',
71
 
72
            // The following columns are unused:
73
            // coursemoduleid, courseid, moduleid, groupid, rating, usermodified.
74
        ], 'privacy:metadata:post');
75
 
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');
79
 
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');
89
 
90
        // We do not report on blog_association because this is just context-related data.
91
 
92
        return $collection;
93
    }
94
 
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();
104
 
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);
118
 
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
        }
133
 
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
        }
148
 
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);
164
 
165
        return $contextlist;
166
    }
167
 
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) {
178
 
179
            $params = ['contextid' => $context->id];
180
 
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";
184
 
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);
190
 
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];
199
 
200
            $sql = "SELECT userid
201
                      FROM {blog_external}
202
                     WHERE userid = :userid";
203
            $userlist->add_from_sql('userid', $sql, $params);
204
 
205
            $sql = "SELECT userid
206
                      FROM {post}
207
                     WHERE userid = :userid";
208
            $userlist->add_from_sql('userid', $sql, $params);
209
 
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
    }
214
 
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;
222
 
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 = [];
229
 
230
        foreach ($contextlist as $context) {
231
            switch ($context->contextlevel) {
232
                case CONTEXT_USER:
233
                    $contextuserid = $context->instanceid;
234
                    $insql = ' > 0';
235
                    $inparams = [];
236
 
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
                        ]);
251
 
252
                        if (empty($entryids)) {
253
                            // This should not happen, as the user context should not have been reported then.
254
                            continue 2;
255
                        }
256
 
257
                        list($insql, $inparams) = $DB->get_in_or_equal($entryids, SQL_PARAMS_NAMED);
258
                    }
259
 
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) {
265
 
266
                        $subject = format_string($record->subject);
267
                        $path = array_merge($rootpath, [get_string('blogentries', 'core_blog'), $subject . " ({$record->id})"]);
268
 
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
                        }
275
 
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
                        }
284
 
285
                        // Rewrite the summary files.
286
                        $summary = writer::with_context($context)->rewrite_pluginfile_urls($path, 'blog', 'post',
287
                            $record->id, $record->summary);
288
 
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();
303
 
304
                        // Export associated tags.
305
                        \core_tag\privacy\provider::export_item_tags($userid, $context, $path, 'core', 'post', $record->id);
306
 
307
                        // Export all comments made on my post.
308
                        \core_comment\privacy\provider::export_comments($context, 'blog', 'format_blog',
309
                            $record->id, $path, false);
310
 
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
                        ];
321
 
322
                        writer::with_context($context)->export_data($path, $entry);
323
                    }
324
                    $recordset->close();
325
 
326
                    // Export external blogs.
327
                    $recordset = $DB->get_recordset('blog_external', ['userid' => $userid]);
328
                    foreach ($recordset as $record) {
329
 
330
                        $path = array_merge($rootpath, [get_string('externalblogs', 'core_blog'),
331
                            $record->name . " ({$record->id})"]);
332
 
333
                        // Export associated tags.
334
                        \core_tag\privacy\provider::export_item_tags($userid, $context, $path, 'core',
335
                            'blog_external', $record->id);
336
 
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
                        ];
346
 
347
                        writer::with_context($context)->export_data($path, $external);
348
                    }
349
                    $recordset->close();
350
                    break;
351
 
352
                case CONTEXT_COURSE:
353
                case CONTEXT_MODULE:
354
                    $associations[] = $context->id;
355
                    break;
356
            }
357
        }
358
 
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]);
373
 
374
            $path = [get_string('privacy:path:blogassociations', 'core_blog')];
375
 
376
            $flushassocs = function($context, $assocs) use ($path) {
377
                writer::with_context($context)->export_data($path, (object) [
378
                    'associations' => $assocs
379
                ]);
380
            };
381
 
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);
387
 
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
            }
395
 
396
            if ($lastcontextid) {
397
                $flushassocs(context::instance_by_id($lastcontextid), $assocs);
398
            }
399
 
400
            $recordset->close();
401
        }
402
    }
403
 
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;
415
 
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
    }
425
 
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 = [];
435
 
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
        }
450
 
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);
462
 
463
            $DB->delete_records_list('blog_association', 'id', $associds);
464
        }
465
    }
466
 
467
    /**
468
     * Delete multiple users within a single context.
469
     *
470
     * @param   approved_userlist       $userlist The approved context and user information to delete information for.
471
     */
472
    public static function delete_data_for_users(\core_privacy\local\request\approved_userlist $userlist) {
473
        global $DB;
474
 
475
        $context = $userlist->get_context();
476
        $userids = $userlist->get_userids();
477
 
478
        if ($context->contextlevel == CONTEXT_USER) {
479
            // If one of the listed users matches this context then delete the blog, associations, and comments.
480
            if (array_search($context->instanceid, $userids) !== false) {
481
                self::delete_all_user_data($context);
482
                \core_comment\privacy\provider::delete_comments_for_all_users($context, 'blog', 'format_blog');
483
                return;
484
            }
485
            \core_comment\privacy\provider::delete_comments_for_users($userlist, 'blog', 'format_blog');
486
        } else {
487
            list($insql, $inparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
488
            $sql = "SELECT ba.id
489
                      FROM {blog_association} ba
490
                      JOIN {post} p ON p.id = ba.blogid
491
                     WHERE ba.contextid = :contextid
492
                       AND p.userid $insql";
493
            $inparams['contextid'] = $context->id;
494
            $associds = $DB->get_fieldset_sql($sql, $inparams);
495
 
496
            if (!empty($associds)) {
497
                list($insql, $inparams) = $DB->get_in_or_equal($associds, SQL_PARAMS_NAMED, 'param', true);
498
                $DB->delete_records_select('blog_association', "id $insql", $inparams);
499
            }
500
        }
501
    }
502
 
503
    /**
504
     * Helper method to delete all user data.
505
     *
506
     * @param context_user $usercontext The user context.
507
     * @return void
508
     */
509
    protected static function delete_all_user_data(context_user $usercontext) {
510
        global $DB;
511
        $userid = $usercontext->instanceid;
512
 
513
        // Delete all blog posts.
514
        $recordset = $DB->get_recordset_select('post', 'userid = :userid AND module IN (:blog, :blogext)', [
515
            'userid' => $userid, 'blog' => 'blog', 'blogext' => 'blog_external']);
516
        foreach ($recordset as $record) {
517
            $entry = new blog_entry(null, $record);
518
            $entry->delete();   // Takes care of files and associations.
519
        }
520
        $recordset->close();
521
 
522
        // Delete all external blogs, and their associated tags.
523
        $DB->delete_records('blog_external', ['userid' => $userid]);
524
        core_tag_tag::delete_instances('core', 'blog_external', $usercontext->id);
525
    }
526
 
527
    /**
528
     * Transform a publish state.
529
     *
530
     * @param string $publishstate The publish state.
531
     * @return string
532
     */
533
    public static function transform_publishstate($publishstate) {
534
        switch ($publishstate) {
535
            case 'draft':
536
                return get_string('publishtonoone', 'core_blog');
537
            case 'site':
538
                return get_string('publishtosite', 'core_blog');
539
            case 'public':
540
                return get_string('publishtoworld', 'core_blog');
541
            default:
542
        }
543
        return get_string('privacy:unknown', 'core_blog');
544
    }
545
}