Search moodle.org's
Developer Documentation

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.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  }