Skip to content

Commit 6f49e90

Browse files
Merge pull request #6645 from christianbeeznest/GH-3244-2
Plugin: BBB: Handle 413 gracefully and compact pre-upload UI - refs #3244
2 parents be283a2 + c13bbe5 commit 6f49e90

File tree

4 files changed

+230
-82
lines changed

4 files changed

+230
-82
lines changed

public/plugin/Bbb/lib/bbb.lib.php

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -543,18 +543,27 @@ public function createMeeting($params)
543543
while ($status === false) {
544544
$result = $this->api->createMeetingWithXmlResponseArray($bbbParams);
545545

546-
if (is_array($result) && (string) ($result['returncode'] ?? '') === 'SUCCESS') {
546+
if ((string)($result['returncode'] ?? '') === 'SUCCESS') {
547547
if ($this->plugin->get('allow_regenerate_recording') === 'true' && !empty($result['internalMeetingID'])) {
548548
$meeting->setInternalMeetingId($result['internalMeetingID']);
549549
$em->flush();
550550
}
551-
552551
return $this->joinMeeting($meetingName, true);
553552
}
554553

555-
// Break condition to avoid infinite loop if API keeps failing
554+
if ((string)($result['returncode'] ?? '') === 'FAILED') {
555+
if ((int)($result['httpCode'] ?? 0) === 413) {
556+
if ($this->debug) {
557+
error_log('BBB createMeeting failed (413): payload too large');
558+
}
559+
} else if ($this->debug) {
560+
error_log('BBB createMeeting failed: '.json_encode($result));
561+
}
562+
break;
563+
}
564+
556565
if ($this->debug) {
557-
error_log('BBB createMeeting failed, response: '.print_r($result, true));
566+
error_log('BBB createMeeting unexpected response: '.print_r($result, true));
558567
}
559568
break;
560569
}

public/plugin/Bbb/lib/bbb_api.php

Lines changed: 58 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
*/
3939

4040
use BigBlueButton\BigBlueButton;
41+
use BigBlueButton\Exceptions\BadResponseException;
4142
use BigBlueButton\Parameters\Config\DocumentOptionsStore;
4243
use BigBlueButton\Parameters\CreateMeetingParameters;
4344
use BigBlueButton\Parameters\JoinMeetingParameters;
@@ -104,48 +105,66 @@ public function getCreateMeetingUrl(array $p): string
104105
*/
105106
public function createMeetingWithXmlResponseArray(array $p): array
106107
{
107-
$cp = new CreateMeetingParameters($p['meetingId'], $p['meetingName']);
108-
$cp->setAttendeePassword($p['attendeePw']);
109-
$cp->setModeratorPassword($p['moderatorPw']);
110-
if (!empty($p['welcomeMsg'])) $cp->setWelcomeMessage($p['welcomeMsg']);
111-
if (isset($p['dialNumber'])) $cp->setDialNumber($p['dialNumber']);
112-
if (isset($p['voiceBridge'])) $cp->setVoiceBridge((int)$p['voiceBridge']);
113-
if (isset($p['webVoice'])) $cp->setWebVoice($p['webVoice']);
114-
if (isset($p['logoutUrl'])) $cp->setLogoutUrl($p['logoutUrl']);
115-
if (isset($p['maxParticipants'])) $cp->setMaxParticipants((int)$p['maxParticipants']);
116-
if (isset($p['record'])) $cp->setRecord((bool)$p['record']);
117-
if (isset($p['duration'])) $cp->setDuration((int)$p['duration']);
118-
119-
if (!empty($p['documents']) && is_array($p['documents'])) {
120-
foreach ($p['documents'] as $doc) {
121-
$options = new DocumentOptionsStore();
122-
$options->addAttribute('removable', (bool) $doc['removable']);
123-
$cp->addPresentation(
124-
$doc['filename'],
125-
file_get_contents($doc['url']),
126-
$doc['filename'],
127-
$options
128-
);
108+
try {
109+
$cp = new CreateMeetingParameters($p['meetingId'], $p['meetingName']);
110+
$cp->setAttendeePassword($p['attendeePw']);
111+
$cp->setModeratorPassword($p['moderatorPw']);
112+
if (!empty($p['welcomeMsg'])) $cp->setWelcomeMessage($p['welcomeMsg']);
113+
if (isset($p['dialNumber'])) $cp->setDialNumber($p['dialNumber']);
114+
if (isset($p['voiceBridge'])) $cp->setVoiceBridge((int)$p['voiceBridge']);
115+
if (isset($p['webVoice'])) $cp->setWebVoice($p['webVoice']);
116+
if (isset($p['logoutUrl'])) $cp->setLogoutUrl($p['logoutUrl']);
117+
if (isset($p['maxParticipants'])) $cp->setMaxParticipants((int)$p['maxParticipants']);
118+
if (isset($p['record'])) $cp->setRecord((bool)$p['record']);
119+
if (isset($p['duration'])) $cp->setDuration((int)$p['duration']);
120+
121+
if (!empty($p['documents']) && is_array($p['documents'])) {
122+
foreach ($p['documents'] as $doc) {
123+
$opts = new DocumentOptionsStore();
124+
$opts->addAttribute('removable', (bool)($doc['removable'] ?? true));
125+
$cp->addPresentation(
126+
$doc['filename'] ?? basename(parse_url($doc['url'], PHP_URL_PATH) ?: 'document'),
127+
file_get_contents($doc['url']),
128+
$doc['filename'] ?? 'document',
129+
$opts
130+
);
131+
}
132+
}
129133

134+
$r = $this->client->createMeeting($cp);
135+
$xml = $r->getRawXml();
136+
137+
return [
138+
'returncode' => (string)$xml->returncode,
139+
'message' => (string)$xml->message,
140+
'messageKey' => (string)$xml->messageKey,
141+
'meetingId' => (string)$xml->meetingID,
142+
'attendeePw' => (string)$xml->attendeePW,
143+
'moderatorPw' => (string)$xml->moderatorPW,
144+
'hasBeenForciblyEnded' => (string)$xml->hasBeenForciblyEnded,
145+
'createTime' => (string)$xml->createTime,
146+
'internalMeetingID' => (string)$xml->internalMeetingID,
147+
];
148+
} catch (BadResponseException $e) {
149+
$http = 0;
150+
if (preg_match('/HTTP code:\s*(\d+)/i', $e->getMessage(), $m)) {
151+
$http = (int)$m[1];
130152
}
153+
return [
154+
'returncode' => 'FAILED',
155+
'messageKey' => $http === 413 ? 'requestEntityTooLarge' : 'badResponse',
156+
'message' => $http === 413
157+
? 'One or more presentations exceed the upload limit on the video-conference server.'
158+
: $e->getMessage(),
159+
'httpCode' => $http,
160+
];
161+
} catch (\Throwable $e) {
162+
return [
163+
'returncode' => 'FAILED',
164+
'messageKey' => 'unexpectedError',
165+
'message' => $e->getMessage(),
166+
];
131167
}
132-
133-
/** @var CreateMeetingResponse $r */
134-
$r = $this->client->createMeeting($cp);
135-
$xml = $r->getRawXml();
136-
137-
// Map XML fields to array
138-
return [
139-
'returncode' => (string) $xml->returncode,
140-
'message' => (string) $xml->message,
141-
'messageKey' => (string) $xml->messageKey,
142-
'meetingId' => (string) $xml->meetingID,
143-
'attendeePw' => (string) $xml->attendeePW,
144-
'moderatorPw' => (string) $xml->moderatorPW,
145-
'hasBeenForciblyEnded' => (string) $xml->hasBeenForciblyEnded,
146-
'createTime' => (string) $xml->createTime,
147-
'internalMeetingID' => (string) $xml->internalMeetingID,
148-
];
149168
}
150169

151170
/**

public/plugin/Bbb/listing.php

Lines changed: 118 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
use Chamilo\CoreBundle\Entity\ConferenceActivity;
1010
use Chamilo\CoreBundle\Entity\ConferenceMeeting;
11+
use Chamilo\CoreBundle\Enums\ActionIcon;
1112
use Chamilo\CourseBundle\Entity\CGroup;
1213

1314
$course_plugin = 'bbb'; // Needed to load plugin lang variables.
@@ -435,51 +436,129 @@
435436
if ($conferenceManager && $allowToEdit) {
436437
$form = new FormValidator('start_conference', 'post', $conferenceUrl);
437438
$form->addElement('hidden', 'action', 'start');
438-
$ajaxUrl = api_get_path(WEB_PATH).'main/inc/ajax/plugin.ajax.php?plugin=bbb&a=list_documents&'.api_get_cidreq();
439+
$ajaxUrl = api_get_path(WEB_PATH).'main/inc/ajax/plugin.ajax.php?plugin=bbb&a=list_documents&'.api_get_cidreq();
440+
$maxTotalMb = (int) api_get_course_plugin_setting('bbb', 'bbb_preupload_max_total_mb', $courseInfo);
441+
if ($maxTotalMb <= 0) { $maxTotalMb = 20; }
442+
443+
$title = htmlspecialchars(get_lang('Pre-upload Documents'), ENT_QUOTES);
444+
$help = htmlspecialchars(get_lang('Select the PDF or PPTX files you want to pre-load as slides for the conference.'), ENT_QUOTES);
445+
$loadingTxt = htmlspecialchars(get_lang('Loading'), ENT_QUOTES);
446+
$noDocsTxt = htmlspecialchars(get_lang('No documents found'), ENT_QUOTES);
447+
$failTxt = htmlspecialchars(get_lang('Failed to load documents'), ENT_QUOTES);
448+
$maxLabel = htmlspecialchars(sprintf(get_lang('Max total: %d MB'), $maxTotalMb), ENT_QUOTES);
449+
450+
$iconHtml = Display::getMdiIcon(
451+
ActionIcon::UPLOAD,
452+
'ch-tool-icon',
453+
null,
454+
ICON_SIZE_MEDIUM,
455+
$title
456+
);
439457

440-
// Pre-upload UI: fetch available course docs and render as checkboxes.
441458
$preuploadHtml = '
442-
<details id="preupload-documents" class="mt-4 border rounded p-3 bg-gray-100">
443-
<summary class="font-semibold cursor-pointer">'.get_lang('Pre-upload Documents').'</summary>
444-
<div class="mt-2 text-gray-700">
445-
<p class="text-sm mb-2">'.get_lang('Select the PDF or PPTX files you want to pre-load as slides for the conference.').'</p>
446-
<div id="preupload-list">'.get_lang('Loading').'…</div>
447-
</div>
448-
</details>
449-
<script>
450-
document.addEventListener("DOMContentLoaded", function() {
451-
var det = document.getElementById("preupload-documents");
452-
if (!det) return;
453-
det.addEventListener("toggle", function once() {
454-
if (!det.open) return;
455-
det.removeEventListener("toggle", once);
456-
fetch("'.$ajaxUrl.'", {credentials:"same-origin"})
459+
<div class="bbb-preupload" style="position:relative;">
460+
<button type="button" id="bbb-pre-btn"
461+
class="btn btn--icon"
462+
title="'.$title.'"
463+
style="position:absolute; right:0; top:-8px;">
464+
'.$iconHtml.'
465+
</button>
466+
467+
<div id="bbb-pre-pop" class="hidden"
468+
style="position:absolute; right:0; top:28px; z-index:50;
469+
width:340px; background:#fff; border:1px solid #e5e7eb;
470+
border-radius:8px; box-shadow:0 8px 24px rgba(0,0,0,.12);
471+
padding:10px;">
472+
<div class="text-sm" style="margin-bottom:6px; color:#475569;">'.$help.'</div>
473+
<div id="preupload-list"
474+
class="text-sm"
475+
style="max-height:220px; overflow:auto; border:1px solid #eef2f7;
476+
border-radius:6px; padding:8px; color:#0f172a;">
477+
'.$loadingTxt.'
478+
</div>
479+
<div class="text-xs" style="margin-top:6px; color:#64748b;">
480+
'.$maxLabel.' — <span id="preupload-total">0</span> MB
481+
</div>
482+
</div>
483+
</div>
484+
485+
<script>
486+
(function(){
487+
var btn = document.getElementById("bbb-pre-btn");
488+
var pop = document.getElementById("bbb-pre-pop");
489+
var list = document.getElementById("preupload-list");
490+
var loaded = false;
491+
var ajax = "'.$ajaxUrl.'";
492+
var maxMb = '.$maxTotalMb.';
493+
494+
function esc(t){
495+
return String(t).replace(/[&<>\"\\\']/g, function(s){
496+
return {"&":"&amp;","<":"&lt;",">":"&gt;","\\"":"&quot;","\\\'":"&#39;"}[s];
497+
});
498+
}
499+
500+
function togglePop(){
501+
if (pop.classList.contains("hidden")) {
502+
pop.classList.remove("hidden");
503+
if (!loaded) {
504+
loaded = true;
505+
fetch(ajax, {credentials:"same-origin"})
457506
.then(function(r){ return r.json(); })
458-
.then(function(docs){
459-
var c = document.getElementById("preupload-list");
460-
if (!Array.isArray(docs) || !docs.length) {
461-
c.innerHTML = \'<p class="text-sm text-gray-500">'.addslashes(get_lang('No documents found.')).'</p>\';
462-
return;
463-
}
464-
var filtered = docs.filter(function(doc){
465-
return (doc.filename || "").match(/\\.(pdf|ppt|pptx|odp)$/i);
466-
});
467-
if (!filtered.length) {
468-
c.innerHTML = \'<p class="text-sm text-gray-500">'.addslashes(get_lang('No documents found.')).'</p>\';
469-
return;
470-
}
471-
c.innerHTML = filtered.map(function(doc){
472-
var data = JSON.stringify({url:doc.url, filename:doc.filename}).replace(/"/g, "&quot;");
473-
return \'<label class="block"><input type="checkbox" name="documents[]" value="\' + data + \'"> \' + (doc.filename || "") + \'</label>\';
474-
}).join("");
475-
})
507+
.then(renderList)
476508
.catch(function(){
477-
document.getElementById("preupload-list").innerHTML =
478-
\'<p class="text-sm text-red-500">'.addslashes(get_lang('Failed to load documents.')).'</p>\';
509+
list.innerHTML = \'<p class="text-sm" style="color:#dc2626">'.$failTxt.'</p>\';
479510
});
480-
});
511+
}
512+
} else {
513+
pop.classList.add("hidden");
514+
}
515+
}
516+
517+
function clickOutside(e){
518+
if (!pop.contains(e.target) && !btn.contains(e.target)) {
519+
pop.classList.add("hidden");
520+
}
521+
}
522+
523+
function renderList(docs){
524+
var items = Array.isArray(docs) ? docs.filter(function(d){
525+
return (d.filename||"").match(/\\.(pdf|ppt|pptx|odp)$/i);
526+
}) : [];
527+
528+
if (!items.length) {
529+
list.innerHTML = \'<p class="text-sm" style="color:#64748b">'.$noDocsTxt.'</p>\';
530+
return;
531+
}
532+
533+
list.innerHTML = items.map(function(doc){
534+
var data = JSON.stringify({url:doc.url, filename:doc.filename, size:doc.size}).replace(/"/g,"&quot;");
535+
return \'<label class="flex items-center gap-2" style="display:flex;align-items:center;gap:.5rem;margin:.25rem 0;">\'
536+
+ \'<input type="checkbox" class="h-4 w-4" name="documents[]" value="\' + data + \'" />\'
537+
+ \'<span class="truncate">\' + esc(doc.filename||"") + \'</span>\'
538+
+ \'</label>\';
539+
}).join("");
540+
541+
list.addEventListener("change", recalcTotal, true);
542+
}
543+
544+
function recalcTotal(){
545+
var boxes = list.querySelectorAll(\'input[type="checkbox"]:checked\');
546+
var total = 0;
547+
boxes.forEach(function(b){
548+
try { var o = JSON.parse(b.value.replace(/&quot;/g, \'"\')); total += (o.size||0); } catch(e){}
481549
});
482-
</script>';
550+
var mb = (total/1048576).toFixed(1);
551+
var out = document.getElementById("preupload-total");
552+
if (out) out.textContent = mb;
553+
554+
var submit = document.querySelector(\'form[name="start_conference"] [type="submit"]\');
555+
if (submit) submit.disabled = (total > maxMb * 1048576);
556+
}
557+
558+
if (btn) btn.addEventListener("click", togglePop);
559+
document.addEventListener("click", clickOutside);
560+
})();
561+
</script>';
483562

484563
$form->addElement('html', $preuploadHtml);
485564
$form->addElement(

public/plugin/Bbb/start.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,48 @@
9696
}
9797
}
9898

99+
$maxTotalMb = (int) api_get_course_plugin_setting('bbb', 'bbb_preupload_max_total_mb', api_get_course_info());
100+
if ($maxTotalMb <= 0) { $maxTotalMb = 20; }
101+
102+
$totalBytes = 0;
103+
if (!empty($_POST['documents']) && is_array($_POST['documents'])) {
104+
$docs = [];
105+
foreach ($_POST['documents'] as $raw) {
106+
$json = html_entity_decode($raw);
107+
$doc = json_decode($json, true);
108+
if (!is_array($doc) || empty($doc['url'])) { continue; }
109+
$totalBytes += (int)($doc['size'] ?? 0);
110+
$docs[] = [
111+
'url' => $doc['url'],
112+
'filename' => $doc['filename'] ?? basename(parse_url($doc['url'], PHP_URL_PATH)),
113+
'downloadable'=> true,
114+
'removable' => true,
115+
];
116+
}
117+
118+
if ($totalBytes > ($maxTotalMb * 1024 * 1024)) {
119+
$message = Display::return_message(
120+
sprintf(get_lang('The total size of selected documents exceeds %d MB.'), $maxTotalMb),
121+
'error'
122+
);
123+
$tpl->assign('message', $message);
124+
$tpl->assign('content', $message);
125+
$tpl->display_one_col_template();
126+
exit;
127+
}
128+
129+
if (!empty($docs)) {
130+
$meetingParams['documents'] = $docs;
131+
}
132+
}
133+
99134
$url = $bbb->createMeeting($meetingParams);
135+
if (!$url) {
136+
$message = Display::return_message(
137+
get_lang('The selected documents exceed the upload limit of the video-conference server. Try fewer/smaller files or contact your administrator.'),
138+
'error'
139+
);
140+
}
100141
}
101142
}
102143

0 commit comments

Comments
 (0)