Files
meetletacbfree/classes/local/roles.php
T

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);
}
}