. /** * BBB-style participant role mapping for Adobe Connect. * * @package mod_meetletacbmarketplace * @copyright 2026 MeetLet * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_meetletacbmarketplace\local; use context; use mod_meetletacbmarketplace\api\service; use stdClass; defined('MOODLE_INTERNAL') || die(); /** * Participant list helpers (BBB pattern: all/role/user → Adobe role). */ class roles { public const ROLE_HOST = 'host'; public const ROLE_PRESENTER = 'presenter'; public const ROLE_PARTICIPANT = 'participant'; public const MODE_INHERIT = 0; public const MODE_OVERRIDE = 1; /** * Map form role id to Adobe permission-id. */ public static function to_adobe_permission(string $role): string { switch ($role) { case self::ROLE_HOST: return service::ROLE_HOST; case self::ROLE_PRESENTER: return service::ROLE_PRESENTER; default: return service::ROLE_PARTICIPANT; } } public static function role_rank(string $role): int { switch ($role) { case self::ROLE_HOST: return 3; case self::ROLE_PRESENTER: return 2; case self::ROLE_PARTICIPANT: return 1; default: return 0; } } /** * Built-in site defaults (no owner user rule). */ public static function builtin_site_defaults(): array { global $DB; $list = [[ 'selectiontype' => 'all', 'selectionid' => 'all', 'role' => self::ROLE_PARTICIPANT, ]]; foreach (['manager', 'coursecreator', 'editingteacher'] as $shortname) { if ($role = $DB->get_record('role', ['shortname' => $shortname], 'id')) { $list[] = [ 'selectiontype' => 'role', 'selectionid' => (string)$role->id, 'role' => self::ROLE_HOST, ]; } } if ($role = $DB->get_record('role', ['shortname' => 'teacher'], 'id')) { $list[] = [ 'selectiontype' => 'role', 'selectionid' => (string)$role->id, 'role' => self::ROLE_PRESENTER, ]; } return $list; } /** * Site-wide default participant rules from plugin config (or built-in). */ public static function get_site_default_rules(): array { $raw = get_config('meetletacbmarketplace', 'default_participants'); if (is_string($raw) && $raw !== '') { $decoded = json_decode($raw, true); if (is_array($decoded) && !empty($decoded)) { return self::normalize_rules($decoded); } } return self::builtin_site_defaults(); } /** * Normalize and sanitize a participant rule list. */ public static function normalize_rules(array $rules): array { $allowed = [self::ROLE_HOST, self::ROLE_PRESENTER, self::ROLE_PARTICIPANT]; $out = []; $hasall = false; foreach ($rules as $rule) { if (!is_array($rule)) { continue; } $type = (string)($rule['selectiontype'] ?? ''); $id = (string)($rule['selectionid'] ?? ''); $role = (string)($rule['role'] ?? self::ROLE_PARTICIPANT); if (!in_array($role, $allowed, true)) { $role = self::ROLE_PARTICIPANT; } if ($type === 'all') { $hasall = true; $out[] = ['selectiontype' => 'all', 'selectionid' => 'all', 'role' => $role]; continue; } if ($type === 'role' && ctype_digit($id) && (int)$id > 0) { $out[] = ['selectiontype' => 'role', 'selectionid' => $id, 'role' => $role]; continue; } if ($type === 'user' && ctype_digit($id) && (int)$id > 0) { $out[] = ['selectiontype' => 'user', 'selectionid' => $id, 'role' => $role]; } } if (!$hasall) { array_unshift($out, [ 'selectiontype' => 'all', 'selectionid' => 'all', 'role' => self::ROLE_PARTICIPANT, ]); } return $out; } /** * Default participant rules for a new / empty activity. */ public static function get_participant_list_default(context $context, ?int $ownerid = null): array { $list = self::get_site_default_rules(); // Activity owner as host when enrolled (activity-only convenience rule). if (!empty($ownerid) && is_enrolled($context, $ownerid)) { $list[] = [ 'selectiontype' => 'user', 'selectionid' => (string)$ownerid, 'role' => self::ROLE_HOST, ]; } return self::normalize_rules($list); } /** * Whether one activity should inherit the site role mapping. */ public static function activity_inherits_defaults(?stdClass $instance): bool { if ($instance === null) { return true; } if (!property_exists($instance, 'participantsmode')) { return false; } return (int)$instance->participantsmode !== self::MODE_OVERRIDE; } /** * Convert rule list → admin matrix keyed by _all / role shortname. * * @return array */ public static function matrix_from_rules(array $rules): array { $matrix = ['_all' => self::ROLE_PARTICIPANT]; foreach (featureaccess::role_records() as $role) { if ($role->shortname !== '') { $matrix[$role->shortname] = self::ROLE_PARTICIPANT; } } $byid = featureaccess::role_records(); foreach (self::normalize_rules($rules) as $rule) { $adobe = $rule['role'] ?? self::ROLE_PARTICIPANT; if (($rule['selectiontype'] ?? '') === 'all') { $matrix['_all'] = $adobe; } else if (($rule['selectiontype'] ?? '') === 'role') { $rid = (int)($rule['selectionid'] ?? 0); if (isset($byid[$rid]) && $byid[$rid]->shortname !== '') { $matrix[$byid[$rid]->shortname] = $adobe; } } } return $matrix; } /** * Convert posted admin matrix → JSON rule list for config storage. * * @param array $data role shortname / _all => adobe role */ public static function rules_json_from_matrix_post(array $data): string { $allowed = [self::ROLE_HOST, self::ROLE_PRESENTER, self::ROLE_PARTICIPANT]; $allrole = (string)($data['_all'] ?? self::ROLE_PARTICIPANT); if (!in_array($allrole, $allowed, true)) { $allrole = self::ROLE_PARTICIPANT; } $list = [[ 'selectiontype' => 'all', 'selectionid' => 'all', 'role' => $allrole, ]]; foreach (featureaccess::role_records() as $role) { if ($role->shortname === '') { continue; } $adobe = (string)($data[$role->shortname] ?? self::ROLE_PARTICIPANT); if (!in_array($adobe, $allowed, true)) { $adobe = self::ROLE_PARTICIPANT; } // Skip redundant participant rows that match the “everyone” default. if ($adobe === $allrole && $adobe === self::ROLE_PARTICIPANT) { continue; } $list[] = [ 'selectiontype' => 'role', 'selectionid' => (string)$role->id, 'role' => $adobe, ]; } return json_encode(self::normalize_rules($list), JSON_UNESCAPED_UNICODE); } /** * Adobe Connect role options for selects/radios. * * @return array value => label */ public static function adobe_role_labels(): array { return [ self::ROLE_HOST => get_string('role_host', 'meetletacbmarketplace'), self::ROLE_PRESENTER => get_string('role_presenter', 'meetletacbmarketplace'), self::ROLE_PARTICIPANT => get_string('role_participant', 'meetletacbmarketplace'), ]; } /** * Render site admin Moodle-role → Adobe-role mapping table. */ public static function render_site_mapping_html(array $rules, string $inputname): string { $matrix = self::matrix_from_rules($rules); $adobelabels = self::adobe_role_labels(); $rolelabels = featureaccess::role_labels(); $html = \html_writer::start_div('meetlet-rolemapping', ['data-meetlet-rolemapping' => '1']); $html .= \html_writer::start_tag('table', [ 'class' => 'generaltable meetlet-access-table meetlet-rolemapping-table', ]); $head = \html_writer::tag('th', get_string('accessrole', 'meetletacbmarketplace')); $head .= \html_writer::tag('th', get_string('adoberole', 'meetletacbmarketplace')); $html .= \html_writer::tag('thead', \html_writer::tag('tr', $head)); $body = ''; // Everyone row first. $body .= self::mapping_row_html( get_string('participant_type_all', 'meetletacbmarketplace'), $inputname . '[_all]', $matrix['_all'] ?? self::ROLE_PARTICIPANT, $adobelabels, '_all' ); foreach ($rolelabels as $shortname => $label) { $body .= self::mapping_row_html( $label, $inputname . '[' . $shortname . ']', $matrix[$shortname] ?? self::ROLE_PARTICIPANT, $adobelabels, $shortname ); } $html .= \html_writer::tag('tbody', $body); $html .= \html_writer::end_tag('table'); $html .= \html_writer::end_div(); return $html; } /** * One mapping table row with Adobe role radios. * * @param array $adobelabels */ protected static function mapping_row_html( string $moodlelabel, string $inputname, string $selected, array $adobelabels, string $rowkey ): string { $cells = \html_writer::tag('th', s($moodlelabel), [ 'scope' => 'row', 'class' => 'meetlet-access-rowhead', ]); $chips = ''; foreach ($adobelabels as $value => $label) { $id = 'meetlet-map-' . preg_replace('/[^a-zA-Z0-9_-]/', '_', $rowkey . '-' . $value); $attrs = [ 'type' => 'radio', 'name' => $inputname, 'id' => $id, 'value' => $value, 'class' => 'meetlet-rolechip__input', ]; if ($selected === $value) { $attrs['checked'] = 'checked'; } $chips .= \html_writer::tag('label', \html_writer::empty_tag('input', $attrs) . \html_writer::span(s($label), 'meetlet-rolechip__text'), [ 'class' => 'meetlet-rolechip', 'for' => $id, 'data-role-value' => $value, ] ); } $group = \html_writer::div($chips, 'meetlet-rolechip-group', [ 'role' => 'radiogroup', 'aria-label' => $moodlelabel, ]); $cells .= \html_writer::tag('td', $group, ['class' => 'meetlet-rolemapping-cell']); return \html_writer::tag('tr', $cells, ['data-role-row' => $rowkey]); } public static function get_participant_list(?stdClass $instance, context $context): array { global $USER; if ($instance === null || self::activity_inherits_defaults($instance) || empty($instance->participants) || $instance->participants === '[]') { return self::get_participant_list_default($context, $USER->id); } $rules = json_decode($instance->participants, true); if (!is_array($rules) || empty($rules)) { // Legacy format support. if (isset($rules['hosts']) || (is_array($rules) && isset($rules[0]) === false && isset($rules['participants']))) { return self::legacy_to_rules($rules); } return self::get_participant_list_default($context, $USER->id); } // Detect legacy associative map. if (isset($rules['hosts']) || isset($rules['presenters'])) { return self::legacy_to_rules($rules); } return $rules; } protected static function legacy_to_rules(array $map): array { $list = [[ 'selectiontype' => 'all', 'selectionid' => 'all', 'role' => self::ROLE_PARTICIPANT, ]]; foreach ($map['hosts'] ?? [] as $id) { $list[] = ['selectiontype' => 'role', 'selectionid' => (string)$id, 'role' => self::ROLE_HOST]; } foreach ($map['presenters'] ?? [] as $id) { $list[] = ['selectiontype' => 'role', 'selectionid' => (string)$id, 'role' => self::ROLE_PRESENTER]; } foreach ($map['participants'] ?? [] as $id) { $list[] = ['selectiontype' => 'role', 'selectionid' => (string)$id, 'role' => self::ROLE_PARTICIPANT]; } return $list; } public static function get_participant_data(context $context, ?stdClass $instance = null): array { $data = [ 'all' => [ 'name' => get_string('participant_type_all', 'meetletacbmarketplace'), 'children' => [], ], 'role' => [ 'name' => get_string('participant_type_role', 'meetletacbmarketplace'), 'children' => self::get_roles_select($context), ], 'user' => [ 'name' => get_string('participant_type_user', 'meetletacbmarketplace'), 'children' => [], ], ]; if ($instance) { $data['user']['children'] = self::get_users_array($context); } return $data; } protected static function get_roles_select(context $context): array { $roles = get_viewable_roles($context); $out = []; foreach ($roles as $id => $name) { $out[$id] = ['id' => $id, 'name' => $name]; } return $out; } protected static function get_users_array(context $context): array { $users = get_enrolled_users($context, '', 0, 'u.id, u.firstname, u.lastname, u.firstnamephonetic, u.lastnamephonetic, u.middlename, u.alternatename'); $out = []; foreach ($users as $u) { $out[$u->id] = ['id' => $u->id, 'name' => fullname($u)]; } return $out; } public static function get_participant_selection_data(): array { return [ 'type_options' => [ 'all' => get_string('participant_type_all', 'meetletacbmarketplace'), 'role' => get_string('participant_type_role', 'meetletacbmarketplace'), 'user' => get_string('participant_type_user', 'meetletacbmarketplace'), ], 'type_selected' => 'role', 'options' => ['all' => '---------------'], 'selected' => 'all', ]; } /** * Resolve Adobe permission for user from participant rules. * * @return string|null Adobe permission-id or null if denied */ public static function resolve_adobe_role(stdClass $instance, context $context, int $userid): ?string { if (has_capability('mod/meetletacbmarketplace:host', $context, $userid)) { return service::ROLE_HOST; } if (has_capability('mod/meetletacbmarketplace:presenter', $context, $userid)) { return service::ROLE_PRESENTER; } $rules = self::get_participant_list($instance, $context); $userroles = get_user_roles($context, $userid); $roleids = []; foreach ($userroles as $r) { $roleids[] = (int)$r->roleid; } $best = null; $bestrank = 0; foreach ($rules as $rule) { $match = false; if (($rule['selectiontype'] ?? '') === 'all') { $match = true; } else if (($rule['selectiontype'] ?? '') === 'role') { $match = in_array((int)$rule['selectionid'], $roleids, true); } else if (($rule['selectiontype'] ?? '') === 'user') { $match = ((int)$rule['selectionid'] === (int)$userid); } if (!$match) { continue; } $role = $rule['role'] ?? self::ROLE_PARTICIPANT; $rank = self::role_rank($role); if ($rank > $bestrank) { $bestrank = $rank; $best = $role; } } if ($best === null) { return !empty($instance->meetingpublic) ? service::ROLE_PARTICIPANT : null; } return self::to_adobe_permission($best); } }