91334a46b5
Co-authored-by: Cursor <cursoragent@cursor.com>
487 lines
17 KiB
PHP
487 lines
17 KiB
PHP
<?php
|
|
// This file is part of Moodle - http://moodle.org/
|
|
//
|
|
// Moodle is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// Moodle is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
/**
|
|
* BBB-style participant role mapping for Adobe Connect.
|
|
*
|
|
* @package mod_meetletacbfree
|
|
* @copyright 2026 MeetLet
|
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
|
*/
|
|
|
|
namespace mod_meetletacbfree\local;
|
|
|
|
use context;
|
|
use mod_meetletacbfree\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('meetletacbfree', '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<string,string>
|
|
*/
|
|
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<string,string> value => label
|
|
*/
|
|
public static function adobe_role_labels(): array {
|
|
return [
|
|
self::ROLE_HOST => get_string('role_host', 'meetletacbfree'),
|
|
self::ROLE_PRESENTER => get_string('role_presenter', 'meetletacbfree'),
|
|
self::ROLE_PARTICIPANT => get_string('role_participant', 'meetletacbfree'),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 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', 'meetletacbfree'));
|
|
$head .= \html_writer::tag('th', get_string('adoberole', 'meetletacbfree'));
|
|
$html .= \html_writer::tag('thead', \html_writer::tag('tr', $head));
|
|
|
|
$body = '';
|
|
// Everyone row first.
|
|
$body .= self::mapping_row_html(
|
|
get_string('participant_type_all', 'meetletacbfree'),
|
|
$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<string,string> $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', 'meetletacbfree'),
|
|
'children' => [],
|
|
],
|
|
'role' => [
|
|
'name' => get_string('participant_type_role', 'meetletacbfree'),
|
|
'children' => self::get_roles_select($context),
|
|
],
|
|
'user' => [
|
|
'name' => get_string('participant_type_user', 'meetletacbfree'),
|
|
'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', 'meetletacbfree'),
|
|
'role' => get_string('participant_type_role', 'meetletacbfree'),
|
|
'user' => get_string('participant_type_user', 'meetletacbfree'),
|
|
],
|
|
'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/meetletacbfree:host', $context, $userid)) {
|
|
return service::ROLE_HOST;
|
|
}
|
|
if (has_capability('mod/meetletacbfree: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);
|
|
}
|
|
}
|