From 93b47710accd6eea88cc1ca7aa243e5d22c7641c Mon Sep 17 00:00:00 2001 From: Mark Nelson Date: Wed, 30 Aug 2017 15:11:20 +0800 Subject: [PATCH] MDL-59977 core: do not directly check 'viewparticipant' capability --- .../block_activity_results.php | 3 +- blocks/participants/block_participants.php | 15 +- course/lib.php | 37 +++- course/recent_form.php | 5 +- course/tests/courselib_test.php | 170 ++++++++++++++++++ enrol/externallib.php | 24 +-- lib/classes/analytics/target/no_teaching.php | 5 +- lib/navigationlib.php | 4 +- lib/upgrade.txt | 2 + message/classes/api.php | 4 + mod/forum/lib.php | 4 +- notes/delete.php | 5 +- notes/edit.php | 5 +- notes/index.php | 5 +- user/externallib.php | 7 +- user/index.php | 10 +- user/messageselect.php | 4 +- 17 files changed, 261 insertions(+), 48 deletions(-) diff --git a/blocks/activity_results/block_activity_results.php b/blocks/activity_results/block_activity_results.php index de9b9ed3795ef..52fb8e90d0baf 100644 --- a/blocks/activity_results/block_activity_results.php +++ b/blocks/activity_results/block_activity_results.php @@ -26,6 +26,7 @@ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/lib/grade/constants.php'); +require_once($CFG->dirroot . '/course/lib.php'); define('B_ACTIVITYRESULTS_NAME_FORMAT_FULL', 1); define('B_ACTIVITYRESULTS_NAME_FORMAT_ID', 2); @@ -328,7 +329,7 @@ public function get_content() { if ($nameformat == B_ACTIVITYRESULTS_NAME_FORMAT_FULL) { if (has_capability('moodle/course:managegroups', $context)) { $grouplink = $CFG->wwwroot.'/group/overview.php?id='.$courseid.'&group='; - } else if (has_capability('moodle/course:viewparticipants', $context)) { + } else if (course_can_view_participants($context)) { $grouplink = $CFG->wwwroot.'/user/index.php?id='.$courseid.'&group='; } else { $grouplink = ''; diff --git a/blocks/participants/block_participants.php b/blocks/participants/block_participants.php index f8214df459210..b1b433b9a98bf 100644 --- a/blocks/participants/block_participants.php +++ b/blocks/participants/block_participants.php @@ -22,6 +22,17 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/course/lib.php'); + +/** + * Participants block + * + * @package block_participants + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ class block_participants extends block_list { function init() { $this->title = get_string('pluginname', 'block_participants'); @@ -48,12 +59,12 @@ function get_content() { $this->content = ''; return $this->content; } else if ($this->page->course->id == SITEID) { - if (!has_capability('moodle/site:viewparticipants', context_system::instance())) { + if (!course_can_view_participants(context_system::instance())) { $this->content = ''; return $this->content; } } else { - if (!has_capability('moodle/course:viewparticipants', $currentcontext)) { + if (!course_can_view_participants($currentcontext)) { $this->content = ''; return $this->content; } diff --git a/course/lib.php b/course/lib.php index 2ccc6f1270a32..b38c641640aeb 100644 --- a/course/lib.php +++ b/course/lib.php @@ -3894,16 +3894,14 @@ function course_get_user_navigation_options($context, $course = null) { // Frontpage settings? if ($isfrontpage) { // We are on the front page, so make sure we use the proper capability (site:viewparticipants). - $options->participants = has_capability('moodle/site:viewparticipants', $sitecontext) || - has_capability('moodle/course:enrolreview', $sitecontext); + $options->participants = course_can_view_participants($sitecontext); $options->badges = !empty($CFG->enablebadges) && has_capability('moodle/badges:viewbadges', $sitecontext); $options->tags = !empty($CFG->usetags) && $isloggedin; $options->search = !empty($CFG->enableglobalsearch) && has_capability('moodle/search:query', $sitecontext); $options->calendar = $isloggedin; } else { // We are in a course, so make sure we use the proper capability (course:viewparticipants). - $options->participants = has_capability('moodle/course:viewparticipants', $context) || - has_capability('moodle/course:enrolreview', $context); + $options->participants = course_can_view_participants($context); $options->badges = !empty($CFG->enablebadges) && !empty($CFG->badges_allowcoursebadges) && has_capability('moodle/badges:viewbadges', $context); // Add view grade report is permitted. @@ -4237,3 +4235,34 @@ function course_check_module_updates_since($cm, $from, $fileareas = array(), $fi return $updates; } + +/** + * Returns true if the user can view the participant page, false otherwise, + * + * @param context $context The context we are checking. + * @return bool + */ +function course_can_view_participants($context) { + $viewparticipantscap = 'moodle/course:viewparticipants'; + if ($context->contextlevel == CONTEXT_SYSTEM) { + $viewparticipantscap = 'moodle/site:viewparticipants'; + } + + return has_any_capability([$viewparticipantscap, 'moodle/course:enrolreview'], $context); +} + +/** + * Checks if a user can view the participant page, if not throws an exception. + * + * @param context $context The context we are checking. + * @throws required_capability_exception + */ +function course_require_view_participants($context) { + if (!course_can_view_participants($context)) { + $viewparticipantscap = 'moodle/course:viewparticipants'; + if ($context->contextlevel == CONTEXT_SYSTEM) { + $viewparticipantscap = 'moodle/site:viewparticipants'; + } + throw new required_capability_exception($context, $viewparticipantscap, 'nopermissions', ''); + } +} diff --git a/course/recent_form.php b/course/recent_form.php index cc2750a7863b5..d3056ea9fa505 100644 --- a/course/recent_form.php +++ b/course/recent_form.php @@ -27,6 +27,7 @@ die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page } +require_once($CFG->dirroot . '/course/lib.php'); require_once($CFG->libdir.'/formslib.php'); class recent_form extends moodleform { @@ -64,9 +65,9 @@ function definition() { } if ($COURSE->id == SITEID) { - $viewparticipants = has_capability('moodle/site:viewparticipants', context_system::instance()); + $viewparticipants = course_can_view_participants(context_system::instance()); } else { - $viewparticipants = has_capability('moodle/course:viewparticipants', $context); + $viewparticipants = course_can_view_participants($context); } if ($viewparticipants) { diff --git a/course/tests/courselib_test.php b/course/tests/courselib_test.php index 702771fe8be13..6d53ba5b8f508 100644 --- a/course/tests/courselib_test.php +++ b/course/tests/courselib_test.php @@ -3904,4 +3904,174 @@ public function test_course_module_bulk_update_calendar_events() { // Update the assign instances for this course. $this->assertTrue(course_module_bulk_update_calendar_events('assign', $course->id)); } + + /** + * Test that a student can view participants in a course they are enrolled in. + */ + public function test_course_can_view_participants_as_student() { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $coursecontext = context_course::instance($course->id); + + $user = $this->getDataGenerator()->create_user(); + $this->getDataGenerator()->enrol_user($user->id, $course->id); + + $this->setUser($user); + + $this->assertTrue(course_can_view_participants($coursecontext)); + } + + /** + * Test that a student in a course can not view participants on the site. + */ + public function test_course_can_view_participants_as_student_on_site() { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + + $user = $this->getDataGenerator()->create_user(); + $this->getDataGenerator()->enrol_user($user->id, $course->id); + + $this->setUser($user); + + $this->assertFalse(course_can_view_participants(context_system::instance())); + } + + /** + * Test that an admin can view participants on the site. + */ + public function test_course_can_view_participants_as_admin_on_site() { + $this->resetAfterTest(); + + $this->setAdminUser(); + + $this->assertTrue(course_can_view_participants(context_system::instance())); + } + + /** + * Test teachers can view participants in a course they are enrolled in. + */ + public function test_course_can_view_participants_as_teacher() { + global $DB; + + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $coursecontext = context_course::instance($course->id); + + $user = $this->getDataGenerator()->create_user(); + $roleid = $DB->get_field('role', 'id', array('shortname' => 'editingteacher')); + $this->getDataGenerator()->enrol_user($user->id, $course->id, $roleid); + + $this->setUser($user); + + $this->assertTrue(course_can_view_participants($coursecontext)); + } + + /** + * Check the teacher can still view the participants page without the 'viewparticipants' cap. + */ + public function test_course_can_view_participants_as_teacher_without_view_participants_cap() { + global $DB; + + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $coursecontext = context_course::instance($course->id); + + $user = $this->getDataGenerator()->create_user(); + $roleid = $DB->get_field('role', 'id', array('shortname' => 'editingteacher')); + $this->getDataGenerator()->enrol_user($user->id, $course->id, $roleid); + + $this->setUser($user); + + // Disable one of the capabilties. + assign_capability('moodle/course:viewparticipants', CAP_PROHIBIT, $roleid, $coursecontext); + + // Should still be able to view the page as they have the 'moodle/course:enrolreview' cap. + $this->assertTrue(course_can_view_participants($coursecontext)); + } + + /** + * Check the teacher can still view the participants page without the 'moodle/course:enrolreview' cap. + */ + public function test_course_can_view_participants_as_teacher_without_enrol_review_cap() { + global $DB; + + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $coursecontext = context_course::instance($course->id); + + $user = $this->getDataGenerator()->create_user(); + $roleid = $DB->get_field('role', 'id', array('shortname' => 'editingteacher')); + $this->getDataGenerator()->enrol_user($user->id, $course->id, $roleid); + + $this->setUser($user); + + // Disable one of the capabilties. + assign_capability('moodle/course:enrolreview', CAP_PROHIBIT, $roleid, $coursecontext); + + // Should still be able to view the page as they have the 'moodle/course:viewparticipants' cap. + $this->assertTrue(course_can_view_participants($coursecontext)); + } + + /** + * Check the teacher can not view the participants page without the required caps. + */ + public function test_course_can_view_participants_as_teacher_without_required_caps() { + global $DB; + + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $coursecontext = context_course::instance($course->id); + + $user = $this->getDataGenerator()->create_user(); + $roleid = $DB->get_field('role', 'id', array('shortname' => 'editingteacher')); + $this->getDataGenerator()->enrol_user($user->id, $course->id, $roleid); + + $this->setUser($user); + + // Disable the capabilities. + assign_capability('moodle/course:viewparticipants', CAP_PROHIBIT, $roleid, $coursecontext); + assign_capability('moodle/course:enrolreview', CAP_PROHIBIT, $roleid, $coursecontext); + + $this->assertFalse(course_can_view_participants($coursecontext)); + } + + /** + * Check that an exception is not thrown if we can view the participants page. + */ + public function test_course_require_view_participants() { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $coursecontext = context_course::instance($course->id); + + $user = $this->getDataGenerator()->create_user(); + $this->getDataGenerator()->enrol_user($user->id, $course->id); + + $this->setUser($user); + + course_require_view_participants($coursecontext); + } + + /** + * Check that an exception is thrown if we can't view the participants page. + */ + public function test_course_require_view_participants_as_student_on_site() { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + + $user = $this->getDataGenerator()->create_user(); + $this->getDataGenerator()->enrol_user($user->id, $course->id); + + $this->setUser($user); + + $this->expectException('required_capability_exception'); + course_require_view_participants(context_system::instance()); + } } diff --git a/enrol/externallib.php b/enrol/externallib.php index 370141a44efb7..d11bfab194fca 100644 --- a/enrol/externallib.php +++ b/enrol/externallib.php @@ -86,6 +86,8 @@ public static function get_enrolled_users_with_capability_parameters() { */ public static function get_enrolled_users_with_capability($coursecapabilities, $options) { global $CFG, $DB; + + require_once($CFG->dirroot . '/course/lib.php'); require_once($CFG->dirroot . "/user/lib.php"); if (empty($coursecapabilities)) { @@ -145,11 +147,8 @@ public static function get_enrolled_users_with_capability($coursecapabilities, $ throw new moodle_exception(get_string('errorcoursecontextnotvalid' , 'webservice', $exceptionparam)); } - if ($courseid == SITEID) { - require_capability('moodle/site:viewparticipants', $context); - } else { - require_capability('moodle/course:viewparticipants', $context); - } + course_require_view_participants($context); + // The accessallgroups capability is needed to use this option. if (!empty($groupid) && groups_is_member($groupid)) { require_capability('moodle/site:accessallgroups', $coursecontext); @@ -293,7 +292,9 @@ public static function get_users_courses_parameters() { * @return array of courses */ public static function get_users_courses($userid) { - global $USER, $DB; + global $CFG, $USER, $DB; + + require_once($CFG->dirroot . '/course/lib.php'); // Do basic automatic PARAM checks on incoming data, using params description // If any problems are found then exceptions are thrown with helpful error messages @@ -312,7 +313,7 @@ public static function get_users_courses($userid) { continue; } - if ($userid != $USER->id and !has_capability('moodle/course:viewparticipants', $context)) { + if ($userid != $USER->id and !course_can_view_participants($context)) { // we need capability to view participants continue; } @@ -520,6 +521,8 @@ public static function get_enrolled_users_parameters() { */ public static function get_enrolled_users($courseid, $options = array()) { global $CFG, $USER, $DB; + + require_once($CFG->dirroot . '/course/lib.php'); require_once($CFG->dirroot . "/user/lib.php"); $params = self::validate_parameters( @@ -600,11 +603,8 @@ public static function get_enrolled_users($courseid, $options = array()) { throw new moodle_exception('errorcoursecontextnotvalid' , 'webservice', '', $exceptionparam); } - if ($courseid == SITEID) { - require_capability('moodle/site:viewparticipants', $context); - } else { - require_capability('moodle/course:viewparticipants', $context); - } + course_require_view_participants($context); + // to overwrite this parameter, you need role:review capability if ($withcapability) { require_capability('moodle/role:review', $coursecontext); diff --git a/lib/classes/analytics/target/no_teaching.php b/lib/classes/analytics/target/no_teaching.php index 7d3714fffb3b0..bacd39a7897a3 100644 --- a/lib/classes/analytics/target/no_teaching.php +++ b/lib/classes/analytics/target/no_teaching.php @@ -63,6 +63,9 @@ public static function get_name() : \lang_string { * @return \core_analytics\prediction_action[] */ public function prediction_actions(\core_analytics\prediction $prediction, $includedetailsaction = false) { + global $CFG; + + require_once($CFG->dirroot . '/course/lib.php'); // No need to call the parent as the parent's action is view details and this target only have 1 feature. $actions = array(); @@ -75,7 +78,7 @@ public function prediction_actions(\core_analytics\prediction $prediction, $incl $actions['viewcourse'] = new \core_analytics\prediction_action('viewcourse', $prediction, $url, $pix, get_string('view')); - if (has_any_capability(['moodle/course:viewparticipants', 'moodle/course:enrolreview'], $sampledata['context'])) { + if (course_can_view_participants($sampledata['context'])) { $url = new \moodle_url('/user/index.php', array('id' => $course->id)); $pix = new \pix_icon('i/cohort', get_string('participants')); $actions['viewparticipants'] = new \core_analytics\prediction_action('viewparticipants', $prediction, diff --git a/lib/navigationlib.php b/lib/navigationlib.php index afcd47ed61bf8..2cbe2d7e021ba 100644 --- a/lib/navigationlib.php +++ b/lib/navigationlib.php @@ -2212,6 +2212,8 @@ protected function load_activity($cm, stdClass $course, navigation_node $activit protected function load_for_user($user=null, $forceforcontext=false) { global $DB, $CFG, $USER, $SITE; + require_once($CFG->dirroot . '/course/lib.php'); + if ($user === null) { // We can't require login here but if the user isn't logged in we don't // want to show anything @@ -2258,7 +2260,7 @@ protected function load_for_user($user=null, $forceforcontext=false) { } else if ($USER->id != $user->id) { // This is the site so add a users node to the root branch. $usersnode = $this->rootnodes['users']; - if (has_capability('moodle/course:viewparticipants', $coursecontext)) { + if (course_can_view_participants($coursecontext)) { $usersnode->action = new moodle_url('/user/index.php', array('id' => $course->id)); } $userviewurl = new moodle_url('/user/profile.php', $baseargs); diff --git a/lib/upgrade.txt b/lib/upgrade.txt index cd4d742fe553a..38772ea50e4e9 100644 --- a/lib/upgrade.txt +++ b/lib/upgrade.txt @@ -44,6 +44,8 @@ information provided here is intended especially for developers. * user_can_view_profile() now also checks the moodle/user:viewalldetails capability. * The core/modal_confirm dialogue has been deprecated. Please use the core/modal_save_cancel dialogue instead. Please ensure you update to use the ModalEvents.save and ModalEvents.cancel events instead of their yes/no counterparts. +* Instead of checking the 'moodle/course:viewparticipants' and 'moodle/site:viewparticipants' capabilities use the + new functions course_can_view_participants() and course_require_view_participants(). === 3.3.1 === diff --git a/message/classes/api.php b/message/classes/api.php index 642353fdc4b1c..206552921262a 100644 --- a/message/classes/api.php +++ b/message/classes/api.php @@ -199,6 +199,10 @@ public static function search_users($userid, $search, $limitnum = 0) { // Make sure to limit searches to enrolled courses. $enrolledcourses = enrol_get_my_courses(array('id', 'cacherev')); $courses = array(); + // Really we want the user to be able to view the participants if they have the capability + // 'moodle/course:viewparticipants' or 'moodle/course:enrolreview', but since the search_courses function + // only takes required parameters we can't. However, the chance of a user having 'moodle/course:enrolreview' but + // *not* 'moodle/course:viewparticipants' are pretty much zero, so it is not worth addressing. if ($arrcourses = \coursecat::search_courses(array('search' => $search), array('limit' => $limitnum), array('moodle/course:viewparticipants'))) { foreach ($arrcourses as $course) { diff --git a/mod/forum/lib.php b/mod/forum/lib.php index 0f080a925194a..03ecffd8c2672 100644 --- a/mod/forum/lib.php +++ b/mod/forum/lib.php @@ -5362,6 +5362,8 @@ function forum_print_latest_discussions($course, $forum, $maxdiscussions = -1, $ $currentgroup = -1, $groupmode = -1, $page = -1, $perpage = 100, $cm = null) { global $CFG, $USER, $OUTPUT; + require_once($CFG->dirroot . '/course/lib.php'); + if (!$cm) { if (!$cm = get_coursemodule_from_instance('forum', $forum->id, $forum->course)) { print_error('invalidcoursemodule'); @@ -5498,7 +5500,7 @@ function forum_print_latest_discussions($course, $forum, $maxdiscussions = -1, $ } } - $canviewparticipants = has_capability('moodle/course:viewparticipants',$context); + $canviewparticipants = course_can_view_participants($context); $canviewhiddentimedposts = has_capability('mod/forum:viewhiddentimedposts', $context); $strdatestring = get_string('strftimerecentfull'); diff --git a/notes/delete.php b/notes/delete.php index 3e0efc41413ae..cb184b5497778 100644 --- a/notes/delete.php +++ b/notes/delete.php @@ -16,6 +16,7 @@ require_once('../config.php'); require_once('lib.php'); +require_once($CFG->dirroot . '/course/lib.php'); $noteid = required_param('id', PARAM_INT); @@ -59,9 +60,7 @@ // Output HTML. $link = null; - if (has_capability('moodle/course:viewparticipants', $context) - || has_capability('moodle/site:viewparticipants', context_system::instance())) { - + if (course_can_view_participants($context) || course_can_view_participants(context_system::instance())) { $link = new moodle_url('/user/index.php', array('id' => $course->id)); } $PAGE->navbar->add(get_string('participants'), $link); diff --git a/notes/edit.php b/notes/edit.php index 88a697f796526..7b97ecc250f63 100644 --- a/notes/edit.php +++ b/notes/edit.php @@ -17,6 +17,7 @@ require_once('../config.php'); require_once('lib.php'); require_once('edit_form.php'); +require_once($CFG->dirroot . '/course/lib.php'); $noteid = optional_param('id', 0, PARAM_INT); @@ -95,9 +96,7 @@ // Output HTML. $link = null; -if (has_capability('moodle/course:viewparticipants', $context) - || has_capability('moodle/site:viewparticipants', context_system::instance())) { - +if (course_can_view_participants($context) || course_can_view_participants(context_system::instance())) { $link = new moodle_url('/user/index.php', array('id' => $course->id)); } $PAGE->navbar->add(get_string('participants'), $link); diff --git a/notes/index.php b/notes/index.php index d9f97758211d8..3c5cd28c6013e 100644 --- a/notes/index.php +++ b/notes/index.php @@ -22,6 +22,7 @@ */ require_once('../config.php'); require_once('lib.php'); +require_once($CFG->dirroot . '/course/lib.php'); $courseid = optional_param('course', SITEID, PARAM_INT); $userid = optional_param('user', 0, PARAM_INT); @@ -110,9 +111,7 @@ $PAGE->set_context(context_course::instance($courseid)); } else { $link = null; - if (has_capability('moodle/course:viewparticipants', $coursecontext) - || has_capability('moodle/site:viewparticipants', $systemcontext)) { - + if (course_can_view_participants($coursecontext) || course_can_view_participants($systemcontext)) { $link = new moodle_url('/user/index.php', array('id' => $course->id)); } } diff --git a/user/externallib.php b/user/externallib.php index 40576ed05a354..d8b24a390387f 100644 --- a/user/externallib.php +++ b/user/externallib.php @@ -1330,6 +1330,7 @@ public static function view_user_list_parameters() { public static function view_user_list($courseid) { global $CFG; require_once($CFG->dirroot . "/user/lib.php"); + require_once($CFG->dirroot . '/course/lib.php'); $params = self::validate_parameters(self::view_user_list_parameters(), array( @@ -1351,11 +1352,7 @@ public static function view_user_list($courseid) { } self::validate_context($context); - if ($course->id == SITEID) { - require_capability('moodle/site:viewparticipants', $context); - } else { - require_capability('moodle/course:viewparticipants', $context); - } + course_require_view_participants($context); user_list_view($course, $context); diff --git a/user/index.php b/user/index.php index 9431d52832859..3b1961c9de65d 100644 --- a/user/index.php +++ b/user/index.php @@ -74,16 +74,10 @@ if ($isfrontpage) { $PAGE->set_pagelayout('admin'); - if (!has_any_capability(['moodle/site:viewparticipants', 'moodle/course:enrolreview'], $systemcontext)) { - // We know they do not have any of the capabilities, so lets throw an exception using the capability with the least access. - throw new required_capability_exception($systemcontext, 'moodle/site:viewparticipants', 'nopermissions', ''); - } + course_require_view_participants($systemcontext); } else { $PAGE->set_pagelayout('incourse'); - if (!has_any_capability(['moodle/course:viewparticipants', 'moodle/course:enrolreview'], $context)) { - // We know they do not have any of the capabilities, so lets throw an exception using the capability with the least access. - throw new required_capability_exception($context, 'moodle/course:viewparticipants', 'nopermissions', ''); - } + course_require_view_participants($context); } // Trigger events. diff --git a/user/messageselect.php b/user/messageselect.php index 99f2d96b44282..9d7412d428c17 100644 --- a/user/messageselect.php +++ b/user/messageselect.php @@ -24,6 +24,7 @@ require_once('../config.php'); require_once($CFG->dirroot.'/message/lib.php'); +require_once($CFG->dirroot . '/course/lib.php'); $id = required_param('id', PARAM_INT); $messagebody = optional_param('messagebody', '', PARAM_CLEANHTML); @@ -115,8 +116,7 @@ } $link = null; -if (has_capability('moodle/course:viewparticipants', $coursecontext) || - has_capability('moodle/site:viewparticipants', $systemcontext)) { +if (course_can_view_participants($coursecontext) || course_can_view_participants($systemcontext)) { $link = new moodle_url("/user/index.php", array('id' => $course->id)); } $PAGE->navbar->add(get_string('participants'), $link);