Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.
   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  }