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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body