VYPR
Moderate severityOSV Advisory· Published Feb 19, 2024· Updated Aug 1, 2024

Msa-24-0004: forum export did not respect activity group settings

CVE-2024-25981

Description

Moodle's forum export ignored Separate Groups mode, leaking forum data across groups to non-editing teachers.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Moodle's forum export ignored Separate Groups mode, leaking forum data across groups to non-editing teachers.

A forum export function in Moodle failed to honor the Separate Groups mode setting, meaning that when a user with the appropriate permissions (by default, non-editing teachers) performed a forum export, the exported data included discussions and posts from all groups, not just the user's own group [1]. The root cause is that the export code did not pass the forum activity's context ID when retrieving users for the export, thereby skipping the group restriction checks that are normally applied in that context [2].

To exploit this vulnerability, an attacker must have the role of a non-editing teacher (or a similar role that is permitted to export forum content) in a course that uses Separate Groups mode. The attacker does not need to be a member of the groups whose data they wish to export [1]. The attack is performed through the standard forum export interface, which will silently return all group data without any warning to the user [2].

The impact is an unauthorized information disclosure of forum posts and discussions from groups that the attacker is not a member of. While the vulnerability is limited to users with export permissions, it violates the expected data isolation provided by Separate Groups mode, which is a key privacy and access control feature in Moodle [1][3].

Moodle released a commit (1c059cb) that fixes the issue. The fix ensures the forum's context ID is correctly passed during the export user search, enabling the proper group restrictions to be applied [2]. Administrators are advised to update their Moodle installations to a patched version. No workaround is documented, and the vulnerability is not currently listed on CISA's Known Exploited Vulnerabilities Catalog.

AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
moodle/moodlePackagist
>= 4.3.0, < 4.3.34.3.3
moodle/moodlePackagist
>= 4.2.0, < 4.2.64.2.6
moodle/moodlePackagist
< 4.1.94.1.9

Affected products

3

Patches

1
1c059cb3fe39

MDL-80504 forum: Fix seperate group mode

https://github.com/moodle/moodleIlya TregubovFeb 8, 2024via ghsa
12 files changed · +306 32
  • enrol/externallib.php+20 9 modified
    @@ -629,6 +629,7 @@ public static function search_users_parameters(): external_function_parameters {
                     'searchanywhere' => new external_value(PARAM_BOOL, 'find a match anywhere, or only at the beginning'),
                     'page' => new external_value(PARAM_INT, 'Page number'),
                     'perpage' => new external_value(PARAM_INT, 'Number per page'),
    +                'contextid' => new external_value(PARAM_INT, 'Context ID', VALUE_DEFAULT, null),
                 ]
             );
         }
    @@ -641,11 +642,12 @@ public static function search_users_parameters(): external_function_parameters {
          * @param bool $searchanywhere Match anywhere in the string
          * @param int $page Page number
          * @param int $perpage Max per page
    +     * @param ?int $contextid Context ID we are in - we might use search on activity level and its group mode can be different from course group mode.
          * @return array An array of users
          * @throws moodle_exception
          */
    -    public static function search_users(int $courseid, string $search, bool $searchanywhere, int $page, int $perpage): array {
    -        global $PAGE, $DB, $CFG;
    +    public static function search_users(int $courseid, string $search, bool $searchanywhere, int $page, int $perpage, ?int $contextid = null): array {
    +        global $PAGE, $CFG;
     
             require_once($CFG->dirroot.'/enrol/locallib.php');
             require_once($CFG->dirroot.'/user/lib.php');
    @@ -657,10 +659,15 @@ public static function search_users(int $courseid, string $search, bool $searcha
                         'search'         => $search,
                         'searchanywhere' => $searchanywhere,
                         'page'           => $page,
    -                    'perpage'        => $perpage
    -                ]
    +                    'perpage'        => $perpage,
    +                    'contextid'      => $contextid,
    +                ],
             );
    -        $context = context_course::instance($params['courseid']);
    +        if (isset($contextid)) {
    +            $context = context::instance_by_id($params['contextid']);
    +        } else {
    +            $context = context_course::instance($params['courseid']);
    +        }
             try {
                 self::validate_context($context);
             } catch (Exception $e) {
    @@ -674,10 +681,14 @@ public static function search_users(int $courseid, string $search, bool $searcha
             $course = get_course($params['courseid']);
             $manager = new course_enrolment_manager($PAGE, $course);
     
    -        $users = $manager->search_users($params['search'],
    -                                        $params['searchanywhere'],
    -                                        $params['page'],
    -                                        $params['perpage']);
    +        $users = $manager->search_users(
    +            $params['search'],
    +            $params['searchanywhere'],
    +            $params['page'],
    +            $params['perpage'],
    +            false,
    +            $params['contextid']
    +        );
     
             $results = [];
             // Add also extra user fields.
    
  • enrol/locallib.php+18 4 modified
    @@ -563,26 +563,40 @@ public function search_other_users($search = '', $searchanywhere = false, $page
          * @param int $page Starting at 0.
          * @param int $perpage Number of users returned per page.
          * @param bool $returnexactcount Return the exact total users using count_record or not.
    +     * @param ?int $contextid Context ID we are in - we might use search on activity level and its group mode can be different from course group mode.
          * @return array with two or three elements:
          *      int totalusers Number users matching the search. (This element only exist if $returnexactcount was set to true)
          *      array users List of user objects returned by the query.
          *      boolean moreusers True if there are still more users, otherwise is False.
          */
         public function search_users(string $search = '', bool $searchanywhere = false, int $page = 0, int $perpage = 25,
    -            bool $returnexactcount = false) {
    +            bool $returnexactcount = false, ?int $contextid = null) {
             global $USER;
     
             [$ufields, $joins, $params, $wherecondition] = $this->get_basic_search_conditions($search, $searchanywhere);
     
    -        $groupmode = groups_get_course_groupmode($this->course);
    -        if ($groupmode == SEPARATEGROUPS && !has_capability('moodle/site:accessallgroups', $this->context)) {
    +        if (isset($contextid)) {
    +            // If contextid is set, we need to determine the group mode that should be used (module or course).
    +            [$context, $course, $cm] = get_context_info_array($contextid);
    +            // If cm instance is returned, then use the group mode from the module, otherwise get the course group mode.
    +            $groupmode = $cm ? groups_get_activity_groupmode($cm, $course) : groups_get_course_groupmode($this->course);
    +        } else {
    +            // Otherwise, default to the group mode of the course.
    +            $context = $this->context;
    +            $groupmode = groups_get_course_groupmode($this->course);
    +        }
    +
    +        if ($groupmode == SEPARATEGROUPS && !has_capability('moodle/site:accessallgroups', $context)) {
                 $groups = groups_get_all_groups($this->course->id, $USER->id, 0, 'g.id');
                 $groupids = array_column($groups, 'id');
    +            if (!$groupids) {
    +                return ['totalusers' => 0, 'users' => [], 'moreusers' => false];
    +            }
             } else {
                 $groupids = [];
             }
     
    -        [$enrolledsql, $enrolledparams] = get_enrolled_sql($this->context, '', $groupids);
    +        [$enrolledsql, $enrolledparams] = get_enrolled_sql($context, '', $groupids);
     
             $fields      = 'SELECT ' . $ufields;
             $countfields = 'SELECT COUNT(u.id)';
    
  • enrol/tests/course_enrolment_manager_test.php+53 10 modified
    @@ -18,6 +18,7 @@
     
     use context_course;
     use course_enrolment_manager;
    +use stdClass;
     
     /**
      * Test course_enrolment_manager parts.
    @@ -557,11 +558,18 @@ public function test_search_users_course_groupmode(): void {
     
             $this->resetAfterTest();
     
    +        // Create the forum.
    +        $record = new stdClass();
    +        $record->introformat = FORMAT_HTML;
    +        $record->course = $this->course->id;
    +        $forum = self::getDataGenerator()->create_module('forum', $record, ['groupmode' => SEPARATEGROUPS]);
    +        $contextid = $DB->get_field('context', 'id', ['instanceid' => $forum->cmid, 'contextlevel' => CONTEXT_MODULE]);
    +
             $teacher = $this->getDataGenerator()->create_and_enrol($this->course, 'teacher');
             $this->getDataGenerator()->create_group_member(['groupid' => $this->groups['group1']->id, 'userid' => $teacher->id]);
             $this->setUser($teacher);
     
    -        $users = (new course_enrolment_manager($PAGE, $this->course))->search_users('', false, 0, 25, true);
    +        $courseusers = (new course_enrolment_manager($PAGE, $this->course))->search_users('', false, 0, 25, true);
             $this->assertEqualsCanonicalizing([
                 $teacher->username,
                 $this->users['user0']->username,
    @@ -570,26 +578,61 @@ public function test_search_users_course_groupmode(): void {
                 $this->users['user22']->username,
                 $this->users['userall']->username,
                 $this->users['usertch']->username,
    -        ], array_column($users['users'], 'username'));
    -        $this->assertEquals(7, $users['totalusers']);
    +        ], array_column($courseusers['users'], 'username'));
    +        $this->assertEquals(7, $courseusers['totalusers']);
     
    -        // Switch course to separate groups.
    +        $forumusers = (new course_enrolment_manager($PAGE, $this->course))->search_users('', false, 0, 25, true, $contextid);
    +        $this->assertEqualsCanonicalizing([
    +            $teacher->username,
    +            $this->users['user1']->username,
    +            $this->users['userall']->username,
    +        ], array_column($forumusers['users'], 'username'));
    +        $this->assertEquals(3, $forumusers['totalusers']);
    +
    +        // Switch course to separate groups and forum to no group.
             $this->course->groupmode = SEPARATEGROUPS;
             update_course($this->course);
    +        set_coursemodule_groupmode($forum->cmid, NOGROUPS);
     
    -        $users = (new course_enrolment_manager($PAGE, $this->course))->search_users('', false, 0, 25, true);
    +        $courseusers = (new course_enrolment_manager($PAGE, $this->course))->search_users('', false, 0, 25, true);
             $this->assertEqualsCanonicalizing([
                 $teacher->username,
                 $this->users['user1']->username,
                 $this->users['userall']->username,
    -        ], array_column($users['users'], 'username'));
    -        $this->assertEquals(3, $users['totalusers']);
    +        ], array_column($courseusers['users'], 'username'));
    +        $this->assertEquals(3, $courseusers['totalusers']);
    +
    +        $forumusers = (new course_enrolment_manager($PAGE, $this->course))->search_users('', false, 0, 25, true, $contextid);
    +        $this->assertEqualsCanonicalizing([
    +            $teacher->username,
    +            $this->users['user0']->username,
    +            $this->users['user1']->username,
    +            $this->users['user21']->username,
    +            $this->users['user22']->username,
    +            $this->users['userall']->username,
    +            $this->users['usertch']->username,
    +        ], array_column($forumusers['users'], 'username'));
    +        $this->assertEquals(7, $forumusers['totalusers']);
    +
    +        set_coursemodule_groupmode($forum->cmid, SEPARATEGROUPS);
     
             // Allow teacher to access all groups.
             $roleid = $DB->get_field('role', 'id', ['shortname' => 'teacher']);
             assign_capability('moodle/site:accessallgroups', CAP_ALLOW, $roleid, context_course::instance($this->course->id)->id);
     
    -        $users = (new course_enrolment_manager($PAGE, $this->course))->search_users('', false, 0, 25, true);
    +        $courseusers = (new course_enrolment_manager($PAGE, $this->course))->search_users('', false, 0, 25, true);
    +        $this->assertEqualsCanonicalizing([
    +            $teacher->username,
    +            $this->users['user0']->username,
    +            $this->users['user1']->username,
    +            $this->users['user21']->username,
    +            $this->users['user22']->username,
    +            $this->users['userall']->username,
    +            $this->users['usertch']->username,
    +        ], array_column($courseusers['users'], 'username'));
    +        $this->assertEquals(7, $courseusers['totalusers']);
    +
    +        $forumusers = (new course_enrolment_manager($PAGE, $this->course))->search_users('', false, 0, 25, true, $contextid);
             $this->assertEqualsCanonicalizing([
                 $teacher->username,
                 $this->users['user0']->username,
    @@ -598,7 +641,7 @@ public function test_search_users_course_groupmode(): void {
                 $this->users['user22']->username,
                 $this->users['userall']->username,
                 $this->users['usertch']->username,
    -        ], array_column($users['users'], 'username'));
    -        $this->assertEquals(7, $users['totalusers']);
    +        ], array_column($forumusers['users'], 'username'));
    +        $this->assertEquals(7, $forumusers['totalusers']);
         }
     }
    
  • enrol/tests/externallib_test.php+66 0 modified
    @@ -20,6 +20,7 @@
     use core_external\external_api;
     use enrol_user_enrolment_form;
     use externallib_advanced_testcase;
    +use stdClass;
     
     defined('MOODLE_INTERNAL') || die();
     
    @@ -1421,6 +1422,71 @@ public function test_search_users() {
             $this->assertCount(0, $result);
         }
     
    +    /**
    +     * Test for core_enrol_external::search_users() when group mode is active.
    +     * @covers ::search_users
    +     */
    +    public function test_search_users_groupmode() {
    +        global $DB;
    +
    +        $this->resetAfterTest();
    +        $datagen = $this->getDataGenerator();
    +
    +        $studentroleid = $DB->get_field('role', 'id', ['shortname' => 'student'], MUST_EXIST);
    +        $teacherroleid = $DB->get_field('role', 'id', ['shortname' => 'teacher'], MUST_EXIST);
    +
    +        $course = $datagen->create_course();
    +
    +        $student1 = $datagen->create_and_enrol($course);
    +        $student2 = $datagen->create_and_enrol($course);
    +        $student3 = $datagen->create_and_enrol($course);
    +        $teacher1 = $datagen->create_and_enrol($course, 'teacher');
    +        $teacher2 = $datagen->create_and_enrol($course, 'teacher');
    +        $teacher3 = $datagen->create_and_enrol($course, 'teacher');
    +        $teacher4 = $datagen->create_and_enrol($course, 'editingteacher');
    +
    +        // Create 2 groups.
    +        $group1 = $datagen->create_group(['courseid' => $course->id]);
    +        $group2 = $datagen->create_group(['courseid' => $course->id]);
    +
    +        // Add the users to the groups.
    +        $datagen->create_group_member(['groupid' => $group1->id, 'userid' => $student1->id]);
    +        $datagen->create_group_member(['groupid' => $group2->id, 'userid' => $student2->id]);
    +        $datagen->create_group_member(['groupid' => $group2->id, 'userid' => $student3->id]);
    +        $datagen->create_group_member(['groupid' => $group1->id, 'userid' => $teacher1->id]);
    +        $datagen->create_group_member(['groupid' => $group2->id, 'userid' => $teacher1->id]);
    +        $datagen->create_group_member(['groupid' => $group1->id, 'userid' => $teacher2->id]);
    +
    +        // Create the forum.
    +        $record = new stdClass();
    +        $record->introformat = FORMAT_HTML;
    +        $record->course = $course->id;
    +        $forum = self::getDataGenerator()->create_module('forum', $record, ['groupmode' => SEPARATEGROUPS]);
    +        $contextid = $DB->get_field('context', 'id', ['instanceid' => $forum->cmid, 'contextlevel' => CONTEXT_MODULE]);
    +
    +        $this->setUser($teacher1);
    +        $result = core_enrol_external::search_users($course->id, 'user', true, 0, 30, $contextid);
    +        $this->assertCount(5, $result);
    +
    +        $this->setUser($teacher2);
    +        $result = core_enrol_external::search_users($course->id, 'user', true, 0, 30, $contextid);
    +        $this->assertCount(3, $result);
    +
    +        $this->setUser($teacher3);
    +        $result = core_enrol_external::search_users($course->id, 'user', true, 0, 30, $contextid);
    +        $this->assertCount(0, $result);
    +
    +        $this->setUser($teacher4);
    +        $result = core_enrol_external::search_users($course->id, 'user', true, 0, 30, $contextid);
    +        $this->assertCount(7, $result);
    +
    +        // Now change the group mode to no groups.
    +        set_coursemodule_groupmode($forum->cmid, NOGROUPS);
    +        $this->setUser($teacher1);
    +        $result = core_enrol_external::search_users($course->id, 'user', true, 0, 30, $contextid);
    +        $this->assertCount(7, $result);
    +    }
    +
         /**
          * Tests the get_potential_users external function (not too much detail because the back-end
          * is covered in another test).
    
  • enrol/upgrade.txt+2 0 modified
    @@ -3,6 +3,8 @@ information provided here is intended especially for developers.
     
     === 4.4 ===
     
    +* Functions core_enrol_external::search_users and course_enrolment_manager::search_users now have extra optional parameter
    +  contextid which allows to search users in a specific activity context. When omitted it searches in the whole course.
     * New find_instance() function has been created. It finds a matching enrolment instance in a given course using provided
      enrolment data (for example cohort idnumber for cohort enrolment). Defaults to null. Override this function in your
      enrolment plugin if you want it to be supported in CSV course upload. Please be aware that sometimes it is not possible
    
  • mod/forum/amd/build/form-user-selector.min.js+1 1 modified
    @@ -5,6 +5,6 @@
      * @copyright  2019 Shamim Rezaie
      * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
      */
    -define("mod_forum/form-user-selector",["jquery","core/ajax","core/templates"],(function($,Ajax,Templates){return{processResults:function(selector,results){var users=[];return $.each(results,(function(index,user){users.push({value:user.id,label:user._label})})),users},transport:function(selector,query,success,failure){var courseid=$(selector).attr("courseid");Ajax.call([{methodname:"core_enrol_search_users",args:{courseid:courseid,search:query,searchanywhere:!0,page:0,perpage:30}}])[0].then((function(results){var promises=[],i=0;return $.each(results,(function(index,user){promises.push(Templates.render("mod_forum/form-user-selector-suggestion",user))})),$.when.apply($.when,promises).then((function(){var args=arguments;$.each(results,(function(index,user){user._label=args[i],i++})),success(results)}))})).fail(failure)}}}));
    +define("mod_forum/form-user-selector",["jquery","core/ajax","core/templates"],(function($,Ajax,Templates){return{processResults:function(selector,results){var users=[];return $.each(results,(function(index,user){users.push({value:user.id,label:user._label})})),users},transport:function(selector,query,success,failure){var courseid=$(selector).attr("courseid"),contextid=$(selector).attr("data-contextid");Ajax.call([{methodname:"core_enrol_search_users",args:{courseid:courseid,search:query,searchanywhere:!0,page:0,perpage:30,contextid:contextid}}])[0].then((function(results){var promises=[],i=0;return $.each(results,(function(index,user){promises.push(Templates.render("mod_forum/form-user-selector-suggestion",user))})),$.when.apply($.when,promises).then((function(){var args=arguments;$.each(results,(function(index,user){user._label=args[i],i++})),success(results)}))})).fail(failure)}}}));
     
     //# sourceMappingURL=form-user-selector.min.js.map
    \ No newline at end of file
    
  • mod/forum/amd/build/form-user-selector.min.js.map+1 1 modified
    @@ -1 +1 @@
    -{"version":3,"file":"form-user-selector.min.js","sources":["../src/form-user-selector.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Enrolled user selector module.\n *\n * @module     mod_forum/form-user-selector\n * @copyright  2019 Shamim Rezaie\n * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery', 'core/ajax', 'core/templates'], function($, Ajax, Templates) {\n    return /** @alias module:mod_forum/form-user-selector */ {\n        processResults: function(selector, results) {\n            var users = [];\n            $.each(results, function(index, user) {\n                users.push({\n                    value: user.id,\n                    label: user._label\n                });\n            });\n            return users;\n        },\n\n        transport: function(selector, query, success, failure) {\n            var promise;\n            var courseid = $(selector).attr('courseid');\n\n            promise = Ajax.call([{\n                methodname: 'core_enrol_search_users',\n                args: {\n                    courseid: courseid,\n                    search: query,\n                    searchanywhere: true,\n                    page: 0,\n                    perpage: 30\n                }\n            }]);\n\n            promise[0].then(function(results) {\n                var promises = [],\n                    i = 0;\n\n                // Render the label.\n                $.each(results, function(index, user) {\n                    promises.push(Templates.render('mod_forum/form-user-selector-suggestion', user));\n                });\n\n                // Apply the label to the results.\n                return $.when.apply($.when, promises).then(function() {\n                    var args = arguments;\n                    $.each(results, function(index, user) {\n                        user._label = args[i];\n                        i++;\n                    });\n                    success(results);\n                    return;\n                });\n\n            }).fail(failure);\n        }\n\n    };\n\n});\n"],"names":["define","$","Ajax","Templates","processResults","selector","results","users","each","index","user","push","value","id","label","_label","transport","query","success","failure","courseid","attr","call","methodname","args","search","searchanywhere","page","perpage","then","promises","i","render","when","apply","arguments","fail"],"mappings":";;;;;;;AAuBAA,sCAAO,CAAC,SAAU,YAAa,mBAAmB,SAASC,EAAGC,KAAMC,iBACP,CACrDC,eAAgB,SAASC,SAAUC,aAC3BC,MAAQ,UACZN,EAAEO,KAAKF,SAAS,SAASG,MAAOC,MAC5BH,MAAMI,KAAK,CACPC,MAAOF,KAAKG,GACZC,MAAOJ,KAAKK,YAGbR,OAGXS,UAAW,SAASX,SAAUY,MAAOC,QAASC,aAEtCC,SAAWnB,EAAEI,UAAUgB,KAAK,YAEtBnB,KAAKoB,KAAK,CAAC,CACjBC,WAAY,0BACZC,KAAM,CACFJ,SAAUA,SACVK,OAAQR,MACRS,gBAAgB,EAChBC,KAAM,EACNC,QAAS,OAIT,GAAGC,MAAK,SAASvB,aACjBwB,SAAW,GACXC,EAAI,SAGR9B,EAAEO,KAAKF,SAAS,SAASG,MAAOC,MAC5BoB,SAASnB,KAAKR,UAAU6B,OAAO,0CAA2CtB,UAIvET,EAAEgC,KAAKC,MAAMjC,EAAEgC,KAAMH,UAAUD,MAAK,eACnCL,KAAOW,UACXlC,EAAEO,KAAKF,SAAS,SAASG,MAAOC,MAC5BA,KAAKK,OAASS,KAAKO,GACnBA,OAEJb,QAAQZ,eAIb8B,KAAKjB"}
    \ No newline at end of file
    +{"version":3,"file":"form-user-selector.min.js","sources":["../src/form-user-selector.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Enrolled user selector module.\n *\n * @module     mod_forum/form-user-selector\n * @copyright  2019 Shamim Rezaie\n * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery', 'core/ajax', 'core/templates'], function($, Ajax, Templates) {\n    return /** @alias module:mod_forum/form-user-selector */ {\n        processResults: function(selector, results) {\n            var users = [];\n            $.each(results, function(index, user) {\n                users.push({\n                    value: user.id,\n                    label: user._label\n                });\n            });\n            return users;\n        },\n\n        transport: function(selector, query, success, failure) {\n            var promise;\n            var courseid = $(selector).attr('courseid');\n            var contextid = $(selector).attr('data-contextid');\n\n            promise = Ajax.call([{\n                methodname: 'core_enrol_search_users',\n                args: {\n                    courseid: courseid,\n                    search: query,\n                    searchanywhere: true,\n                    page: 0,\n                    perpage: 30,\n                    contextid: contextid,\n                }\n            }]);\n\n            promise[0].then(function(results) {\n                var promises = [],\n                    i = 0;\n\n                // Render the label.\n                $.each(results, function(index, user) {\n                    promises.push(Templates.render('mod_forum/form-user-selector-suggestion', user));\n                });\n\n                // Apply the label to the results.\n                return $.when.apply($.when, promises).then(function() {\n                    var args = arguments;\n                    $.each(results, function(index, user) {\n                        user._label = args[i];\n                        i++;\n                    });\n                    success(results);\n                    return;\n                });\n\n            }).fail(failure);\n        }\n\n    };\n\n});\n"],"names":["define","$","Ajax","Templates","processResults","selector","results","users","each","index","user","push","value","id","label","_label","transport","query","success","failure","courseid","attr","contextid","call","methodname","args","search","searchanywhere","page","perpage","then","promises","i","render","when","apply","arguments","fail"],"mappings":";;;;;;;AAuBAA,sCAAO,CAAC,SAAU,YAAa,mBAAmB,SAASC,EAAGC,KAAMC,iBACP,CACrDC,eAAgB,SAASC,SAAUC,aAC3BC,MAAQ,UACZN,EAAEO,KAAKF,SAAS,SAASG,MAAOC,MAC5BH,MAAMI,KAAK,CACPC,MAAOF,KAAKG,GACZC,MAAOJ,KAAKK,YAGbR,OAGXS,UAAW,SAASX,SAAUY,MAAOC,QAASC,aAEtCC,SAAWnB,EAAEI,UAAUgB,KAAK,YAC5BC,UAAYrB,EAAEI,UAAUgB,KAAK,kBAEvBnB,KAAKqB,KAAK,CAAC,CACjBC,WAAY,0BACZC,KAAM,CACFL,SAAUA,SACVM,OAAQT,MACRU,gBAAgB,EAChBC,KAAM,EACNC,QAAS,GACTP,UAAWA,cAIX,GAAGQ,MAAK,SAASxB,aACjByB,SAAW,GACXC,EAAI,SAGR/B,EAAEO,KAAKF,SAAS,SAASG,MAAOC,MAC5BqB,SAASpB,KAAKR,UAAU8B,OAAO,0CAA2CvB,UAIvET,EAAEiC,KAAKC,MAAMlC,EAAEiC,KAAMH,UAAUD,MAAK,eACnCL,KAAOW,UACXnC,EAAEO,KAAKF,SAAS,SAASG,MAAOC,MAC5BA,KAAKK,OAASU,KAAKO,GACnBA,OAEJd,QAAQZ,eAIb+B,KAAKlB"}
    \ No newline at end of file
    
  • mod/forum/amd/src/form-user-selector.js+3 1 modified
    @@ -37,6 +37,7 @@ define(['jquery', 'core/ajax', 'core/templates'], function($, Ajax, Templates) {
             transport: function(selector, query, success, failure) {
                 var promise;
                 var courseid = $(selector).attr('courseid');
    +            var contextid = $(selector).attr('data-contextid');
     
                 promise = Ajax.call([{
                     methodname: 'core_enrol_search_users',
    @@ -45,7 +46,8 @@ define(['jquery', 'core/ajax', 'core/templates'], function($, Ajax, Templates) {
                         search: query,
                         searchanywhere: true,
                         page: 0,
    -                    perpage: 30
    +                    perpage: 30,
    +                    contextid: contextid,
                     }
                 }]);
     
    
  • mod/forum/classes/form/export_form.php+1 1 modified
    @@ -53,6 +53,7 @@ public function definition() {
                 'ajax' => 'mod_forum/form-user-selector',
                 'multiple' => true,
                 'noselectionstring' => get_string('allusers', 'mod_forum'),
    +            'data-contextid' => $forum->get_context()->id,
                 'courseid' => $forum->get_course_id(),
                     'valuehtmlcallback' => function($value) {
                         global $OUTPUT;
    @@ -69,7 +70,6 @@ public function definition() {
             ];
             $mform->addElement('autocomplete', 'useridsselected', get_string('users'), [], $options);
     
    -        // Get the discussions on this forum.
             $vaultfactory = \mod_forum\local\container::get_vault_factory();
             $discussionvault = $vaultfactory->get_discussion_vault();
             $discussions = array_map(function($discussion) {
    
  • mod/forum/classes/local/vaults/discussion.php+18 3 modified
    @@ -26,6 +26,7 @@
     
     defined('MOODLE_INTERNAL') || die();
     
    +use mod_forum\local\container;
     use mod_forum\local\entities\forum as forum_entity;
     use mod_forum\local\entities\discussion as discussion_entity;
     
    @@ -93,9 +94,23 @@ protected function from_db_records(array $results) {
          * @return  array
          */
         public function get_all_discussions_in_forum(forum_entity $forum, string $sort = null): ?array {
    -        $records = $this->get_db()->get_records(self::TABLE, [
    -            'forum' => $forum->get_id(),
    -        ], $sort ?? '');
    +        global $USER;
    +        $options = ['forum' => $forum->get_id()];
    +
    +        $managerfactory = container::get_manager_factory();
    +        $capabilitymanager = $managerfactory->get_capability_manager($forum);
    +
    +        $select = "forum = :forum";
    +
    +        if ($forum->is_in_group_mode() && !$capabilitymanager->can_access_all_groups($USER)) {
    +            $allowedgroups = groups_get_activity_allowed_groups($forum->get_course_module_record());
    +            $allowedgroups = implode(",", array_keys($allowedgroups));
    +            if (!$allowedgroups) {
    +                return [];
    +            }
    +            $select .= " AND groupid IN ($allowedgroups)";
    +        }
    +        $records = $this->get_db()->get_records_select(self::TABLE, $select, $options, $sort ?? '');
     
             return $this->transform_db_records_to_entities($records);
         }
    
  • mod/forum/export.php+1 2 modified
    @@ -192,8 +192,7 @@ function($exportdata) use ($fields, $striphtml, $humandates, $canviewfullname, $
     
     // It is possible that the following fields have been provided in the URL.
     $userids = array_filter($userids, static function(int $userid) use ($course, $cm): bool {
    -    $user = core_user::get_user($userid, '*', MUST_EXIST);
    -    return $cm->effectivegroupmode != SEPARATEGROUPS || user_can_view_profile($user, $course);
    +    return $cm->effectivegroupmode != SEPARATEGROUPS || groups_user_groups_visible($course, $userid, $cm);
     });
     $form->set_data(['useridsselected' => $userids, 'discussionids' => $discussionids, 'from' => $from, 'to' => $to]);
     
    
  • mod/forum/tests/behat/forum_export.feature+122 0 modified
    @@ -48,3 +48,125 @@ Feature: Export forum
         # This will fail if an exception is thrown. This is the best we can do without the ability to use the download. Hence, there is no "Then" step.
         And I click on "Export" "button"
         And I log out
    +
    +  Scenario: Group mode is respected when exporting discussions
    +    Given the following "groups" exist:
    +      | name | course | idnumber |
    +      | G1   | C1     | G1       |
    +      | G2   | C1     | G2       |
    +    And the following "users" exist:
    +      | username | firstname | lastname | email |
    +      | teachera | Teacher   | A | teacherA@example.com |
    +      | teacherb | Teacher   | B | teacherB@example.com |
    +      | teacherc | Teacher   | C | teacherC@example.com |
    +      | teacherd | Teacher   | D | teacherD@example.com |
    +    And the following "course enrolments" exist:
    +      | user     | course | role           |
    +      | teachera | C1     | teacher        |
    +      | teacherb | C1     | teacher        |
    +      | teacherc | C1     | teacher        |
    +      | teacherd | C1     | teacher        |
    +    And the following "group members" exist:
    +      | user        | group |
    +      | teachera    | G1  |
    +      | teachera    | G2  |
    +      | teacherb    | G1  |
    +      | teacherc    | G2  |
    +    And the following "activities" exist:
    +      | activity | course | idnumber | name                    | intro                      | type    | section | groupmode |
    +      | forum    | C1     | 00001    | Separate groups forum   | Standard forum description | general | 1       | 1         |
    +    And the following "mod_forum > discussions" exist:
    +      | user     | forum                 | name                 | message           | group |
    +      | teachera | Separate groups forum | Discussion 1 Group 1 | Test post message | G1    |
    +      | teacherb | Separate groups forum | Discussion 2 Group 1 | Test post message | G1    |
    +      | teachera | Separate groups forum | Discussion 1 Group 2 | Test post message | G2    |
    +      | teacherc | Separate groups forum | Discussion 2 Group 2 | Test post message | G2    |
    +    And I am on the "Separate groups forum" "forum activity" page logged in as teacher1
    +    And I navigate to "Export" in current page administration
    +    When I expand the "Users" autocomplete
    +    # Editing teacher can see all users and discussions.
    +    Then I should see "Teacher A" in the "Users" "autocomplete"
    +    And I should see "Teacher B" in the "Users" "autocomplete"
    +    And I should see "Teacher C" in the "Users" "autocomplete"
    +    And I should see "Teacher D" in the "Users" "autocomplete"
    +    And I should see "Teacher 1" in the "Users" "autocomplete"
    +    And I should see "Student 1" in the "Users" "autocomplete"
    +    And I press the escape key
    +    And I expand the "Discussions" autocomplete
    +    And I should see "Discussion 1 Group 1" in the "Discussions" "autocomplete"
    +    And I should see "Discussion 2 Group 1" in the "Discussions" "autocomplete"
    +    And I should see "Discussion 1 Group 2" in the "Discussions" "autocomplete"
    +    And I should see "Discussion 2 Group 2" in the "Discussions" "autocomplete"
    +    And I click on "Export" "button"
    +
    +    And I am on the "Separate groups forum" "forum activity" page logged in as teachera
    +    And I navigate to "Export" in current page administration
    +    When I expand the "Users" autocomplete
    +    # Teacher A is is in both groups.
    +    Then I should see "Teacher A" in the "Users" "autocomplete"
    +    And I should see "Teacher B" in the "Users" "autocomplete"
    +    And I should see "Teacher C" in the "Users" "autocomplete"
    +    And I should not see "Teacher D" in the "Users" "autocomplete"
    +    And I should not see "Teacher 1" in the "Users" "autocomplete"
    +    And I should not see "Student 1" in the "Users" "autocomplete"
    +    And I press the escape key
    +    And I expand the "Discussions" autocomplete
    +    And I should see "Discussion 1 Group 1" in the "Discussions" "autocomplete"
    +    And I should see "Discussion 2 Group 1" in the "Discussions" "autocomplete"
    +    And I should see "Discussion 1 Group 2" in the "Discussions" "autocomplete"
    +    And I should see "Discussion 2 Group 2" in the "Discussions" "autocomplete"
    +    And I click on "Export" "button"
    +
    +    And I am on the "Separate groups forum" "forum activity" page logged in as teacherb
    +    And I navigate to "Export" in current page administration
    +    When I expand the "Users" autocomplete
    +    # Teacher B is in group 1.
    +    Then I should see "Teacher A" in the "Users" "autocomplete"
    +    And I should see "Teacher B" in the "Users" "autocomplete"
    +    And I should not see "Teacher C" in the "Users" "autocomplete"
    +    And I should not see "Teacher D" in the "Users" "autocomplete"
    +    And I should not see "Teacher 1" in the "Users" "autocomplete"
    +    And I should not see "Student 1" in the "Users" "autocomplete"
    +    And I press the escape key
    +    And I expand the "Discussions" autocomplete
    +    And I should see "Discussion 1 Group 1" in the "Discussions" "autocomplete"
    +    And I should see "Discussion 2 Group 1" in the "Discussions" "autocomplete"
    +    And I should not see "Discussion 1 Group 2" in the "Discussions" "autocomplete"
    +    And I should not see "Discussion 2 Group 2" in the "Discussions" "autocomplete"
    +    And I click on "Export" "button"
    +
    +    And I am on the "Separate groups forum" "forum activity" page logged in as teacherc
    +    And I navigate to "Export" in current page administration
    +    When I expand the "Users" autocomplete
    +    # Teacher C is in group 2.
    +    Then I should see "Teacher A" in the "Users" "autocomplete"
    +    And I should not see "Teacher B" in the "Users" "autocomplete"
    +    And I should see "Teacher C" in the "Users" "autocomplete"
    +    And I should not see "Teacher D" in the "Users" "autocomplete"
    +    And I should not see "Teacher 1" in the "Users" "autocomplete"
    +    And I should not see "Student 1" in the "Users" "autocomplete"
    +    And I press the escape key
    +    And I expand the "Discussions" autocomplete
    +    And I should not see "Discussion 1 Group 1" in the "Discussions" "autocomplete"
    +    And I should not see "Discussion 2 Group 1" in the "Discussions" "autocomplete"
    +    And I should see "Discussion 1 Group 2" in the "Discussions" "autocomplete"
    +    And I should see "Discussion 2 Group 2" in the "Discussions" "autocomplete"
    +    And I click on "Export" "button"
    +
    +    And I am on the "Separate groups forum" "forum activity" page logged in as teacherd
    +    And I navigate to "Export" in current page administration
    +    When I expand the "Users" autocomplete
    +    # Teacher D is in no group.
    +    Then I should not see "Teacher A" in the "Users" "autocomplete"
    +    And I should not see "Teacher B" in the "Users" "autocomplete"
    +    And I should not see "Teacher C" in the "Users" "autocomplete"
    +    And I should not see "Teacher D" in the "Users" "autocomplete"
    +    And I should not see "Teacher 1" in the "Users" "autocomplete"
    +    And I should not see "Student 1" in the "Users" "autocomplete"
    +    And I press the escape key
    +    And I expand the "Discussions" autocomplete
    +    And I should not see "Discussion 1 Group 1" in the "Discussions" "autocomplete"
    +    And I should not see "Discussion 2 Group 1" in the "Discussions" "autocomplete"
    +    And I should not see "Discussion 1 Group 2" in the "Discussions" "autocomplete"
    +    And I should not see "Discussion 2 Group 2" in the "Discussions" "autocomplete"
    +    And I click on "Export" "button"
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

7

News mentions

0

No linked articles in our index yet.