Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.
   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   * Privacy Subsystem implementation for core_group.
  19   *
  20   * @package    core_group
  21   * @category   privacy
  22   * @copyright  2018 Shamim Rezaie <shamim@moodle.com>
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  namespace core_group\privacy;
  27  
  28  defined('MOODLE_INTERNAL') || die();
  29  
  30  use core_privacy\local\metadata\collection;
  31  use core_privacy\local\request\approved_contextlist;
  32  use core_privacy\local\request\approved_userlist;
  33  use core_privacy\local\request\contextlist;
  34  use core_privacy\local\request\transform;
  35  use core_privacy\local\request\userlist;
  36  
  37  /**
  38   * Privacy Subsystem implementation for core_group.
  39   *
  40   * @copyright  2018 Shamim Rezaie <shamim@moodle.com>
  41   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  42   */
  43  class provider implements
  44          // Groups store user data.
  45          \core_privacy\local\metadata\provider,
  46  
  47          // The group subsystem contains user's group memberships.
  48          \core_privacy\local\request\subsystem\provider,
  49  
  50          // The group subsystem can provide information to other plugins.
  51          \core_privacy\local\request\subsystem\plugin_provider,
  52  
  53          // This plugin is capable of determining which users have data within it.
  54          \core_privacy\local\request\core_userlist_provider,
  55          \core_privacy\local\request\shared_userlist_provider
  56      {
  57  
  58      /**
  59       * Returns meta data about this system.
  60       *
  61       * @param   collection $collection The initialised collection to add items to.
  62       * @return  collection A listing of user data stored through this system.
  63       */
  64      public static function get_metadata(collection $collection) : collection {
  65          $collection->add_database_table('groups_members', [
  66              'groupid' => 'privacy:metadata:groups:groupid',
  67              'userid' => 'privacy:metadata:groups:userid',
  68              'timeadded' => 'privacy:metadata:groups:timeadded',
  69          ], 'privacy:metadata:groups');
  70  
  71          $collection->link_subsystem('core_message', 'privacy:metadata:core_message');
  72  
  73          return $collection;
  74      }
  75  
  76      /**
  77       * Writes user data to the writer for the user to download.
  78       *
  79       * @param \context  $context    The context to export data for.
  80       * @param string    $component  The component that is calling this function. Empty string means no component.
  81       * @param array     $subcontext The sub-context in which to export this data.
  82       * @param int       $itemid     Optional itemid associated with component.
  83       */
  84      public static function export_groups(\context $context, string $component, array $subcontext = [], int $itemid = 0) {
  85          global $DB, $USER;
  86  
  87          if (!$context instanceof \context_course) {
  88              return;
  89          }
  90  
  91          $subcontext[] = get_string('groups', 'core_group');
  92  
  93          $sql = "SELECT gm.id, gm.timeadded, gm.userid, g.name, gm.groupid
  94                    FROM {groups_members} gm
  95                    JOIN {groups} g ON gm.groupid = g.id
  96                   WHERE g.courseid = :courseid
  97                         AND gm.component = :component
  98                         AND gm.userid = :userid";
  99          $params = [
 100              'courseid'  => $context->instanceid,
 101              'component' => $component,
 102              'userid'    => $USER->id
 103          ];
 104  
 105          if ($itemid) {
 106              $sql .= ' AND gm.itemid = :itemid';
 107              $params['itemid'] = $itemid;
 108          }
 109  
 110          $groups = $DB->get_records_sql($sql, $params);
 111  
 112          $groupstoexport = array_map(function($group) {
 113              return (object) [
 114                  'name' => format_string($group->name),
 115                  'timeadded' => transform::datetime($group->timeadded),
 116              ];
 117          }, $groups);
 118  
 119          if (!empty($groups)) {
 120              \core_privacy\local\request\writer::with_context($context)
 121                      ->export_data($subcontext, (object) [
 122                          'groups' => $groupstoexport,
 123                      ]);
 124  
 125              foreach ($groups as $group) {
 126                  // Export associated conversations to this group.
 127                  \core_message\privacy\provider::export_conversations($USER->id, 'core_group', 'groups',
 128                      $context, [], $group->groupid);
 129              }
 130          }
 131      }
 132  
 133      /**
 134       * Deletes all group memberships for a specified context and component.
 135       *
 136       * @param \context  $context    Details about which context to delete group memberships for.
 137       * @param string    $component  Component to delete. Empty string means no component (manual group memberships).
 138       * @param int       $itemid     Optional itemid associated with component.
 139       */
 140      public static function delete_groups_for_all_users(\context $context, string $component, int $itemid = 0) {
 141          global $DB;
 142  
 143          if (!$context instanceof \context_course) {
 144              return;
 145          }
 146  
 147          if (!$DB->record_exists('groups', ['courseid' => $context->instanceid])) {
 148              return;
 149          }
 150  
 151          $select = "component = :component AND groupid IN (SELECT g.id FROM {groups} g WHERE courseid = :courseid)";
 152          $params = ['component' => $component, 'courseid' => $context->instanceid];
 153  
 154          if ($itemid) {
 155              $select .= ' AND itemid = :itemid';
 156              $params['itemid'] = $itemid;
 157          }
 158  
 159          // Delete the group conversations.
 160          $groups = $DB->get_records_select('groups_members', $select, $params);
 161          foreach ($groups as $group) {
 162              \core_message\privacy\provider::delete_conversations_for_all_users($context, 'core_group', 'groups', $group->groupid);
 163          }
 164  
 165          // Remove members from the group.
 166          $DB->delete_records_select('groups_members', $select, $params);
 167  
 168          // Purge the group and grouping cache for users.
 169          \cache_helper::purge_by_definition('core', 'user_group_groupings');
 170      }
 171  
 172      /**
 173       * Deletes all records for a user from a list of approved contexts.
 174       *
 175       * @param approved_contextlist  $contextlist    Contains the user ID and a list of contexts to be deleted from.
 176       * @param string                $component      Component to delete from. Empty string means no component (manual memberships).
 177       * @param int                   $itemid         Optional itemid associated with component.
 178       */
 179      public static function delete_groups_for_user(approved_contextlist $contextlist, string $component, int $itemid = 0) {
 180          global $DB;
 181  
 182          $userid = $contextlist->get_user()->id;
 183  
 184          $contextids = $contextlist->get_contextids();
 185  
 186          if (!$contextids) {
 187              return;
 188          }
 189  
 190          list($contextsql, $contextparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED);
 191          $contextparams += ['contextcourse' => CONTEXT_COURSE];
 192          $groupselect = "SELECT g.id
 193                            FROM {groups} g
 194                            JOIN {context} ctx ON g.courseid = ctx.instanceid AND ctx.contextlevel = :contextcourse
 195                           WHERE ctx.id $contextsql";
 196  
 197          if (!$DB->record_exists_sql($groupselect, $contextparams)) {
 198              return;
 199          }
 200  
 201          $select = "userid = :userid AND component = :component AND groupid IN ({$groupselect})";
 202          $params = ['userid' => $userid, 'component' => $component] + $contextparams;
 203  
 204          if ($itemid) {
 205              $select .= ' AND itemid = :itemid';
 206              $params['itemid'] = $itemid;
 207          }
 208  
 209          // Delete the group conversations.
 210          $groups = $DB->get_records_select('groups_members', $select, $params);
 211          foreach ($groups as $group) {
 212              \core_message\privacy\provider::delete_conversations_for_user($contextlist, 'core_group', 'groups', $group->groupid);
 213          }
 214  
 215          // Remove members from the group.
 216          $DB->delete_records_select('groups_members', $select, $params);
 217  
 218          // Invalidate the group and grouping cache for the user.
 219          \cache_helper::invalidate_by_definition('core', 'user_group_groupings', array(), array($userid));
 220      }
 221  
 222      /**
 223       * Add the list of users who are members of some groups in the specified constraints.
 224       *
 225       * @param   userlist    $userlist   The userlist to add the users to.
 226       * @param   string      $component  The component to check.
 227       * @param   int         $itemid     Optional itemid associated with component.
 228       */
 229      public static function get_group_members_in_context(userlist $userlist, string $component, int $itemid = 0) {
 230          $context = $userlist->get_context();
 231  
 232          if (!$context instanceof \context_course) {
 233              return;
 234          }
 235  
 236          // Group members in the given context.
 237          $sql = "SELECT gm.userid
 238                    FROM {groups_members} gm
 239                    JOIN {groups} g ON gm.groupid = g.id
 240                   WHERE g.courseid = :courseid AND gm.component = :component";
 241          $params = [
 242              'courseid'      => $context->instanceid,
 243              'component'     => $component
 244          ];
 245  
 246          if ($itemid) {
 247              $sql .= ' AND gm.itemid = :itemid';
 248              $params['itemid'] = $itemid;
 249          }
 250  
 251          $userlist->add_from_sql('userid', $sql, $params);
 252  
 253          // Get the users with some group conversation in this context.
 254          \core_message\privacy\provider::add_conversations_in_context($userlist, 'core_group', 'groups', $itemid);
 255      }
 256  
 257      /**
 258       * Deletes all records for multiple users within a single context.
 259       *
 260       * @param approved_userlist $userlist   The approved context and user information to delete information for.
 261       * @param string            $component  Component to delete from. Empty string means no component (manual memberships).
 262       * @param int               $itemid     Optional itemid associated with component.
 263       */
 264      public static function delete_groups_for_users(approved_userlist $userlist, string $component, int $itemid = 0) {
 265          global $DB;
 266  
 267          $context = $userlist->get_context();
 268          $userids = $userlist->get_userids();
 269  
 270          if (!$context instanceof \context_course) {
 271              return;
 272          }
 273  
 274          list($usersql, $userparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
 275  
 276          $groupselect = "SELECT id FROM {groups} WHERE courseid = :courseid";
 277          $groupparams = ['courseid' => $context->instanceid];
 278  
 279          $select = "component = :component AND userid {$usersql} AND groupid IN ({$groupselect})";
 280          $params = ['component' => $component] + $groupparams + $userparams;
 281  
 282          if ($itemid) {
 283              $select .= ' AND itemid = :itemid';
 284              $params['itemid'] = $itemid;
 285          }
 286  
 287          // Delete the group conversations for these users.
 288          $groups = $DB->get_records_select('groups_members', $select, $params);
 289          foreach ($groups as $group) {
 290              \core_message\privacy\provider::delete_conversations_for_users($userlist, 'core_group', 'groups', $group->groupid);
 291          }
 292  
 293          $DB->delete_records_select('groups_members', $select, $params);
 294  
 295          // Invalidate the group and grouping cache for the user.
 296          \cache_helper::invalidate_by_definition('core', 'user_group_groupings', array(), $userids);
 297      }
 298  
 299      /**
 300       * Get the list of contexts that contain group membership for the specified user.
 301       *
 302       * @param   int     $userid     The user to search.
 303       * @param   string  $component  The component to check.
 304       * @param   int     $itemid     Optional itemid associated with component.
 305       * @return  contextlist         The contextlist containing the list of contexts.
 306       */
 307      public static function get_contexts_for_group_member(int $userid, string $component, int $itemid = 0) {
 308          $contextlist = new contextlist();
 309  
 310          $sql = "SELECT ctx.id
 311                    FROM {groups_members} gm
 312                    JOIN {groups} g ON gm.groupid = g.id
 313                    JOIN {context} ctx ON g.courseid = ctx.instanceid AND ctx.contextlevel = :contextcourse
 314                   WHERE gm.userid = :userid AND gm.component = :component";
 315  
 316          $params = [
 317              'contextcourse' => CONTEXT_COURSE,
 318              'userid'        => $userid,
 319              'component'     => $component
 320          ];
 321  
 322          if ($itemid) {
 323              $sql .= ' AND gm.itemid = :itemid';
 324              $params['itemid'] = $itemid;
 325          }
 326  
 327          $contextlist->add_from_sql($sql, $params);
 328  
 329          // Get the contexts where the userid has group conversations.
 330          \core_message\privacy\provider::add_contexts_for_conversations($contextlist, $userid, 'core_group', 'groups', $itemid);
 331  
 332          return $contextlist;
 333      }
 334  
 335      /**
 336       * Get the list of users who have data within a context.
 337       *
 338       * @param   int $userid The user to search.
 339       * @return  contextlist The contextlist containing the list of contexts used in this plugin.
 340       */
 341      public static function get_contexts_for_userid(int $userid) : contextlist {
 342          return static::get_contexts_for_group_member($userid, '');
 343      }
 344  
 345      /**
 346       * Get the list of users who have data within a context.
 347       *
 348       * @param   userlist    $userlist   The userlist containing the list of users who have data in this context/plugin combination.
 349       */
 350      public static function get_users_in_context(userlist $userlist) {
 351          $context = $userlist->get_context();
 352  
 353          if (!$context instanceof \context_course) {
 354              return;
 355          }
 356  
 357          static::get_group_members_in_context($userlist, '');
 358      }
 359  
 360      /**
 361       * Export all user data for the specified user, in the specified contexts.
 362       *
 363       * @param approved_contextlist $contextlist The approved contexts to export information for.
 364       */
 365      public static function export_user_data(approved_contextlist $contextlist) {
 366          $contexts = $contextlist->get_contexts();
 367  
 368          foreach ($contexts as $context) {
 369              static::export_groups($context, '');
 370          }
 371      }
 372  
 373      /**
 374       * Delete all data for all users in the specified context.
 375       *
 376       * @param context $context The specific context to delete data for.
 377       */
 378      public static function delete_data_for_all_users_in_context(\context $context) {
 379          static::delete_groups_for_all_users($context, '');
 380      }
 381  
 382      /**
 383       * Delete all user data for the specified user, in the specified contexts.
 384       *
 385       * @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
 386       */
 387      public static function delete_data_for_user(approved_contextlist $contextlist) {
 388          static::delete_groups_for_user($contextlist, '');
 389      }
 390  
 391      /**
 392       * Delete multiple users within a single context.
 393       *
 394       * @param   approved_userlist   $userlist   The approved context and user information to delete information for.
 395       */
 396      public static function delete_data_for_users(approved_userlist $userlist) {
 397          static::delete_groups_for_users($userlist, '');
 398      }
 399  
 400  }