Skip to content

Plugin: Add config-based control for global conference roles visibility in BBB - refs #3498 #6336

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jul 9, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions public/main/gradebook/lib/be/category.class.php
Original file line number Diff line number Diff line change
@@ -2008,8 +2008,8 @@ public function lockAllItems($locked)
*/
public static function generateUserCertificate(
GradebookCategory $category,
int $user_id,
bool $sendNotification = false,
int $user_id,
bool $sendNotification = false,
bool $skipGenerationIfExists = false
) {
$user_id = (int) $user_id;
@@ -2096,7 +2096,13 @@ public static function generateUserCertificate(
}
}

if (!empty($fileWasGenerated) && !empty($my_certificate['publish'])) {
$isOwner = api_get_user_id() == $user_id;
$isPlatformAdmin = api_is_platform_admin();
$isCourseAdmin = api_is_course_admin($courseId);

$canViewCertificate = $isOwner || $isPlatformAdmin || $isCourseAdmin || !empty($my_certificate['publish']);

if (!empty($fileWasGenerated) && $canViewCertificate) {
$certificates = '';
$exportToPDF = null;
$pdfUrl = null;
5 changes: 3 additions & 2 deletions public/main/inc/ajax/plugin.ajax.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<?php
/* For licensing terms, see /license.txt */

use Chamilo\CoreBundle\Entity\AccessUrlRelPlugin;
use Chamilo\CoreBundle\Framework\Container;
use Michelf\MarkdownExtra;
use Chamilo\CoreBundle\Entity\Plugin;
@@ -79,6 +78,7 @@
$appPlugin = new AppPlugin();

if ($action === 'install') {
// Call the install logic inside the plugin itself.
$appPlugin->install($pluginTitle);

$plugin
@@ -91,7 +91,8 @@
$plugin->setSource(Plugin::SOURCE_OFFICIAL);
}

$em->persist($plugin);
// ✅ Removed: persist($plugin) here
// The install() method of the plugin handles persistence already.
} elseif ($plugin && $action === 'uninstall') {
$appPlugin->uninstall($pluginTitle);

2 changes: 2 additions & 0 deletions public/plugin/Bbb/lang/english.php
Original file line number Diff line number Diff line change
@@ -74,3 +74,5 @@
$strings['RoomClosedComment'] = ' ';
$strings['meeting_duration'] = 'Meeting duration (in minutes)';
$strings['big_blue_button_students_start_conference_in_groups'] = 'Allow students to start conference in their groups.';
$strings['hide_conference_link'] = 'Hide conference link in course tool';
$strings['hide_conference_link_comment'] = 'Show or hide a block with a link to the videoconference next to the join button, to allow users to copy it and paste it in another browser window or invite others. Authentication will still be necessary to access non-public conferences.';
39 changes: 26 additions & 13 deletions public/plugin/Bbb/lib/bbb.lib.php
Original file line number Diff line number Diff line change
@@ -279,29 +279,42 @@ public function getMaxUsersLimit()
}
$courseLimit = 0;
$sessionLimit = 0;
// Check the extra fields for this course and session
// Session limit takes priority over course limit
// Course limit takes priority over global limit

// Check course extra field
if (!empty($this->courseId)) {
$extraField = new ExtraField('course');
$fieldId = $extraField->get_all(
$fieldIdList = $extraField->get_all(
array('variable = ?' => 'plugin_bbb_course_users_limit')
);
$extraValue = new ExtraFieldValue('course');
$value = $extraValue->get_values_by_handler_and_field_id($this->courseId, $fieldId[0]['id']);
if (!empty($value['value'])) {
$courseLimit = (int) $value['value'];

if (!empty($fieldIdList)) {
$fieldId = $fieldIdList[0]['id'] ?? null;
if ($fieldId) {
$extraValue = new ExtraFieldValue('course');
$value = $extraValue->get_values_by_handler_and_field_id($this->courseId, $fieldId);
if (!empty($value['value'])) {
$courseLimit = (int) $value['value'];
}
}
}
}

// Check session extra field
if (!empty($this->sessionId)) {
$extraField = new ExtraField('session');
$fieldId = $extraField->get_all(
$fieldIdList = $extraField->get_all(
array('variable = ?' => 'plugin_bbb_session_users_limit')
);
$extraValue = new ExtraFieldValue('session');
$value = $extraValue->get_values_by_handler_and_field_id($this->sessionId, $fieldId[0]['id']);
if (!empty($value['value'])) {
$sessionLimit = (int) $value['value'];

if (!empty($fieldIdList)) {
$fieldId = $fieldIdList[0]['id'] ?? null;
if ($fieldId) {
$extraValue = new ExtraFieldValue('session');
$value = $extraValue->get_values_by_handler_and_field_id($this->sessionId, $fieldId);
if (!empty($value['value'])) {
$sessionLimit = (int) $value['value'];
}
}
}
}

112 changes: 112 additions & 0 deletions public/plugin/Bbb/lib/bbb_plugin.class.php
Original file line number Diff line number Diff line change
@@ -2,8 +2,10 @@

/* For licensing terms, see /license.txt */

use Chamilo\CoreBundle\Entity\AccessUrlRelPlugin;
use Chamilo\CoreBundle\Entity\ConferenceMeeting;
use Chamilo\CoreBundle\Entity\ConferenceRecording;
use Chamilo\CoreBundle\Framework\Container;
use Chamilo\CourseBundle\Entity\CCourseSetting;
use Chamilo\CoreBundle\Entity\Course;

@@ -63,6 +65,7 @@ protected function __construct()
'disable_course_settings' => 'boolean',
'meeting_duration' => 'text',
'delete_recordings_on_course_delete' => 'boolean',
'hide_conference_link' => 'boolean',
]
);

@@ -284,6 +287,115 @@ private function deleteRecording(string $recordId): void
@file_get_contents($url);
}

/**
* Installs the plugin
*/
public function install(): void

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function install has a Cognitive Complexity of 16 (exceeds 5 allowed). Consider refactoring.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Method install has 50 lines of code (exceeds 25 allowed). Consider refactoring.

{
$entityManager = Database::getManager();

$pluginRepo = Container::getPluginRepository();
$plugin = $pluginRepo->findOneByTitle($this->get_name());

if (!$plugin) {
// Create the plugin only if it does not exist
$plugin = new \Chamilo\CoreBundle\Entity\Plugin();
$plugin->setTitle($this->get_name());
$plugin->setInstalled(true);
$plugin->setInstalledVersion($this->get_version());
$plugin->setSource(\Chamilo\CoreBundle\Entity\Plugin::SOURCE_OFFICIAL);

$entityManager->persist($plugin);
$entityManager->flush();
} else {
// Ensure Doctrine manages it in the current UnitOfWork
$plugin = $entityManager->merge($plugin);
}

// Check if the plugin has relations for access URLs
$accessUrlRepo = Container::getAccessUrlRepository();
$accessUrlRelPluginRepo = Container::getAccessUrlRelPluginRepository();

$accessUrls = $accessUrlRepo->findAll();

foreach ($accessUrls as $accessUrl) {
$rel = $accessUrlRelPluginRepo->findOneBy([
'plugin' => $plugin,
'url' => $accessUrl,
]);

if (!$rel) {
$rel = new AccessUrlRelPlugin();
$rel->setPlugin($plugin);
$rel->setUrl($accessUrl);
$rel->setActive(true);

$configuration = [];
foreach ($this->fields as $name => $type) {
$defaultValue = '';

if (is_array($type)) {
$defaultValue = $type['type'] === 'boolean' ? 'false' : '';
} else {
switch ($type) {
case 'boolean':
case 'checkbox':
$defaultValue = 'false';
break;
default:
$defaultValue = '';
break;
}
}

$configuration[$name] = $defaultValue;
}

$rel->setConfiguration($configuration);

$entityManager->persist($rel);
}
}

$entityManager->flush();
}

public function canCurrentUserSeeGlobalConferenceLink(): bool

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Method canCurrentUserSeeGlobalConferenceLink has 29 lines of code (exceeds 25 allowed). Consider refactoring.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function canCurrentUserSeeGlobalConferenceLink has a Cognitive Complexity of 15 (exceeds 5 allowed). Consider refactoring.

{
$allowedStatuses = $this->get('global_conference_allow_roles') ?? [];

if (empty($allowedStatuses)) {
return api_is_platform_admin();
}

foreach ($allowedStatuses as $status) {
switch ((int) $status) {
case PLATFORM_ADMIN:
if (api_is_platform_admin()) {
return true;
}
break;
case COURSEMANAGER:
if (api_is_teacher()) {
return true;
}
break;
case STUDENT:
if (api_is_student()) {
return true;
}
break;
case STUDENT_BOSS:
if (api_is_student_boss()) {
return true;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid too many return statements within this method.

}
break;
}
}

return false;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid too many return statements within this method.

}

public function get_name(): string
{
return 'Bbb';
4 changes: 4 additions & 0 deletions public/plugin/Bbb/listing.php
Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@
require_once __DIR__.'/config.php';

$plugin = BbbPlugin::create();
$canSeeShareableConferenceLink = $plugin->canCurrentUserSeeGlobalConferenceLink();
$tool_name = $plugin->get_lang('Videoconference');
$em = Database::getManager();
$meetingRepository = $em->getRepository(ConferenceMeeting::class);
@@ -446,6 +447,9 @@
$tpl->assign('message', $message);
$tpl->assign('form', $formToString);
$tpl->assign('enter_conference_links', $urlList);
$tpl->assign('can_see_share_link', $canSeeShareableConferenceLink);
$tpl->assign('plugin', $plugin);
$tpl->assign('is_course_context', api_get_course_int_id() > 0);

$content = $tpl->fetch('Bbb/view/listing.tpl');

183 changes: 99 additions & 84 deletions public/plugin/Bbb/view/listing.tpl
Original file line number Diff line number Diff line change
@@ -12,107 +12,122 @@
</style>
{% autoescape false %}
<div class ="row">
{% if bbb_status == true %}
<div class ="col-md-12" style="text-align:center">
<div class="w-full px-4">
{% if bbb_status %}
<div class="text-center space-y-4">
{{ form }}
{% if show_join_button == true %}
{{ enter_conference_links.0 }}
<br />
<strong>{{ 'UrlMeetingToShare'| get_plugin_lang('BBBPlugin') }}</strong>
<div class="well">
<div class="form--inline">
<div class="form-group">
<input id="share_button"
type="text"
style="width:600px"
class="form-control" readonly value="{{ conference_url }}">
<button onclick="copyTextToClipBoard('share_button');" class="btn btn--plain">
<span class="fa fa-copy"></span> {{ 'Copy text' | get_lang }}
</button>
</div>
</div>
</div>
<p>
<span id="users_online" class="label label-warning">
{{ 'XUsersOnLine'| get_plugin_lang('BBBPlugin') | format(users_online) }}
</span>
</p>
{% set hide_conference_link = plugin.get('hide_conference_link')|trim|lower|default('false') %}
{% set can_see_share_link = (not is_course_context|default(false)) or (is_course_context and hide_conference_link != 'true') %}
{% if max_users_limit > 0 %}
{% if conference_manager == true %}
<p>{{ 'MaxXUsersWarning' | get_plugin_lang('BBBPlugin') | format(max_users_limit) }}</p>
{% elseif users_online >= max_users_limit/2 %}
<p>{{ 'MaxXUsersWarning' | get_plugin_lang('BBBPlugin') | format(max_users_limit) }}</p>
{% endif %}
{% endif %}
{% if show_join_button %}
<a href="{{ conference_url }}" target="_blank"
class="inline-block bg-primary text-white font-semibold px-6 py-2 rounded-lg shadow hover:opacity-90 transition">
{{ 'EnterConference'|get_plugin_lang('BBBPlugin') }}
</a>
{% endif %}
{% if show_join_button and can_see_share_link %}
<h3 class="mt-4 text-lg font-semibold text-gray-90">
{{ 'UrlMeetingToShare'|get_plugin_lang('BBBPlugin') }}
</h3>
<div class="flex justify-center items-center gap-2 mt-2">
<input id="share_button"
type="text"
value="{{ conference_url }}"
readonly
class="w-full max-w-xl px-4 py-2 border border-gray-25 rounded-lg shadow-sm text-gray-90 text-sm bg-white">
<button onclick="copyTextToClipBoard('share_button');"
class="px-4 py-2 bg-gray-15 hover:bg-gray-20 rounded-lg text-sm font-medium text-gray-90">
<i class="fa fa-copy mr-1"></i> {{ 'Copy text' | get_lang }}
</button>
</div>
{% endif %}
<p class="mt-2 text-sm text-gray-90">
<span id="users_online" class="inline-block bg-warning text-warning-button-text px-2 py-1 rounded-full">
{{ 'XUsersOnLine'| get_plugin_lang('BBBPlugin') | format(users_online) }}
</span>
</p>
{% if max_users_limit > 0 %}
<p class="text-sm mt-1 text-danger">
{{ 'MaxXUsersWarning' | get_plugin_lang('BBBPlugin') | format(max_users_limit) }}
</p>
{% elseif max_users_limit > 0 %}
{% if conference_manager == true %}
<p>{{ 'MaxXUsersReachedManager' | get_plugin_lang('BBBPlugin') | format(max_users_limit) }}</p>
<p class="text-sm mt-1 text-danger">
{% if conference_manager %}
{{ 'MaxXUsersReachedManager' | get_plugin_lang('BBBPlugin') | format(max_users_limit) }}
{% elseif users_online > 0 %}
<p>{{ 'MaxXUsersReached' | get_plugin_lang('BBBPlugin') | format(max_users_limit) }}</p>
{{ 'MaxXUsersReached' | get_plugin_lang('BBBPlugin') | format(max_users_limit) }}
{% endif %}
</p>
{% endif %}
</div>
<div class ="col-md-12">
<div class="page-header">
<h2>{{ 'RecordList'| get_plugin_lang('BBBPlugin') }}</h2>
</div>
<table class="table">
<tr>
<th>{{ 'CreatedAt'| get_plugin_lang('BBBPlugin') }}</th>
<th>{{ 'Status'| get_lang }}</th>
<th>{{ 'Records'| get_plugin_lang('BBBPlugin') }}</th>
{% if allow_to_edit %}
<th>{{ 'Actions'| get_lang }}</th>
{% endif %}
</tr>
{% for meeting in meetings %}
<tr>
<!-- td>{{ meeting.id }}</td -->
{% if meeting.visibility == 0 %}
<td class="muted">{{ meeting.created_at }}</td>
{% else %}
<td>{{ meeting.created_at }}</td>
{% endif %}
<td>
{% if meeting.status == 1 %}
<span class="label label-success">{{ 'MeetingOpened'|get_plugin_lang('BBBPlugin') }}</span>
{% else %}
<span class="label label-info">{{ 'MeetingClosed'|get_plugin_lang('BBBPlugin') }}</span>
<div class="mt-10">
<h2 class="text-xl font-bold border-b border-gray-25 pb-2 mb-4 text-gray-90">
{{ 'RecordList'| get_plugin_lang('BBBPlugin') }}
</h2>
<div class="overflow-x-auto">
<table class="min-w-full border text-sm text-left text-gray-90 bg-white shadow rounded-lg">
<thead class="bg-gray-15">
<tr>
<th class="px-4 py-2">{{ 'CreatedAt'| get_plugin_lang('BBBPlugin') }}</th>
<th class="px-4 py-2">{{ 'Status'| get_lang }}</th>
<th class="px-4 py-2">{{ 'Records'| get_plugin_lang('BBBPlugin') }}</th>
{% if allow_to_edit %}
<th class="px-4 py-2">{{ 'Actions'| get_lang }}</th>
{% endif %}
</td>
<td>
{% if meeting.record == 1 %}
{# Record list #}
</tr>
</thead>
<tbody>
{% for meeting in meetings %}
<tr class="border-t border-gray-25">
<td class="px-4 py-2 {% if meeting.visibility == 0 %} text-fontdisabled {% endif %}">
{{ meeting.created_at }}
</td>
<td class="px-4 py-2">
{% if meeting.status == 1 %}
<span class="inline-block bg-success text-success-button-text px-2 py-1 rounded-full">
{{ 'MeetingOpened'|get_plugin_lang('BBBPlugin') }}
</span>
{% else %}
<span class="inline-block bg-info text-info-button-text px-2 py-1 rounded-full">
{{ 'MeetingClosed'|get_plugin_lang('BBBPlugin') }}
</span>
{% endif %}
</td>
<td class="px-4 py-2">
{% if meeting.record == 1 %}
{{ meeting.show_links }}
{% else %}
{{ 'NoRecording'|get_plugin_lang('BBBPlugin') }}
{% endif %}
</td>
{% if allow_to_edit %}
<td>
{% if meeting.status == 1 %}
<a class="btn btn--plain" href="{{ meeting.end_url }} ">
{% else %}
<span class="text-fontdisabled">{{ 'NoRecording'|get_plugin_lang('BBBPlugin') }}</span>
{% endif %}
</td>
{% if allow_to_edit %}
<td class="px-4 py-2 space-x-2">
{% if meeting.status == 1 %}
<a href="{{ meeting.end_url }}"
class="text-danger hover:underline">
{{ 'CloseMeeting'|get_plugin_lang('BBBPlugin') }}
</a>
{% endif %}
{{ meeting.action_links }}
{% endif %}
{{ meeting.action_links }}
</td>
{% endif %}
</tr>
{% endfor %}
</table>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% else %}
<div class ="col-md-12" style="text-align:center">
{{ 'ServerIsNotRunning' | get_plugin_lang('BBBPlugin') }}
{% else %}
<div class="text-center text-danger font-semibold mt-8">
{{ 'ServerIsNotRunning' | get_plugin_lang('BBBPlugin') }}
</div>
{% endif %}
{% endif %}
</div>
{% endautoescape %}
6 changes: 6 additions & 0 deletions src/CoreBundle/Framework/Container.php
Original file line number Diff line number Diff line change
@@ -11,6 +11,7 @@
use Chamilo\CoreBundle\Helpers\AccessUrlHelper;
use Chamilo\CoreBundle\Helpers\ContainerHelper;
use Chamilo\CoreBundle\Helpers\ThemeHelper;
use Chamilo\CoreBundle\Repository\AccessUrlRelPluginRepository;
use Chamilo\CoreBundle\Repository\AssetRepository;
use Chamilo\CoreBundle\Repository\CareerRepository;
use Chamilo\CoreBundle\Repository\CourseCategoryRepository;
@@ -282,6 +283,11 @@ public static function getAccessUrlRepository(): AccessUrlRepository
return self::$container->get(AccessUrlRepository::class);
}

public static function getAccessUrlRelPluginRepository(): AccessUrlRelPluginRepository
{
return self::$container->get(AccessUrlRelPluginRepository::class);
}

public static function getAnnouncementAttachmentRepository(): CAnnouncementAttachmentRepository
{
return self::$container->get(CAnnouncementAttachmentRepository::class);

Unchanged files with check annotations Beta

/**
* @template-implements ProviderInterface<Session>
*/
class UserSessionSubscriptionsStateProvider implements ProviderInterface

Check failure on line 25 in src/CoreBundle/State/UserSessionSubscriptionsStateProvider.php

GitHub Actions / PHP 8.2 Test on ubuntu-latest

MethodSignatureMismatch

src/CoreBundle/State/UserSessionSubscriptionsStateProvider.php:25:7: MethodSignatureMismatch: Method Chamilo\CoreBundle\State\UserSessionSubscriptionsStateProvider::provide with return type 'iterable<mixed, mixed>' is different to return type 'array<array-key, mixed>|null|object' of inherited method ApiPlatform\State\ProviderInterface::provide (see https://psalm.dev/042)
{
public function __construct(
private readonly UserHelper $userHelper,
return $courseItems;
}, $results);
$flatItems = array_merge(...$items);

Check failure on line 174 in src/CoreBundle/Controller/Admin/SessionAdminController.php

GitHub Actions / PHP 8.2 Test on ubuntu-latest

NamedArgumentNotAllowed

src/CoreBundle/Controller/Admin/SessionAdminController.php:174:37: NamedArgumentNotAllowed: Method array_merge called with named unpacked array array<array-key, list<array{course: array{id: int|null, title: string}, session: array{endDate: null|string, id: int|null, startDate: null|string, title: string}, user: array{id: int|null, name: string}}>> (array with string keys) (see https://psalm.dev/268)
return $this->json([
'items' => $flatItems,