Adobe Connect Bridge Marketplace edition — initial release

This commit is contained in:
2026-09-01 22:43:54 +03:30
commit 555cd725f7
93 changed files with 20833 additions and 0 deletions
+97
View File
@@ -0,0 +1,97 @@
<?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/>.
/**
* Ajax: SSO URL to view uploaded meeting content in Adobe Connect.
*
* @package mod_meetletacbmarketplace
* @copyright 2026 MeetLet <info@meetlet.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_meetletacbmarketplace\external;
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_single_structure;
use core_external\external_value;
use mod_meetletacbmarketplace\api\service;
use mod_meetletacbmarketplace\local\config;
use mod_meetletacbmarketplace\local\content;
use mod_meetletacbmarketplace\local\featureaccess;
use mod_meetletacbmarketplace\local\instance;
use mod_meetletacbmarketplace\local\roles;
use mod_meetletacbmarketplace\local\sso;
use mod_meetletacbmarketplace\local\usererror;
use moodle_exception;
defined('MOODLE_INTERNAL') || die();
/**
* Prepare Adobe Connect content view URL with session for current user.
*/
class get_content_launch_url extends external_api {
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters([
'cmid' => new external_value(PARAM_INT, 'Course module id'),
'contentscoid' => new external_value(PARAM_INT, 'Content sco id'),
'groupid' => new external_value(PARAM_INT, 'Group id', VALUE_DEFAULT, 0),
]);
}
public static function execute(int $cmid, int $contentscoid, int $groupid = 0): array {
global $USER;
$params = self::validate_parameters(self::execute_parameters(), [
'cmid' => $cmid,
'contentscoid' => $contentscoid,
'groupid' => $groupid,
]);
\mod_meetletacbmarketplace\registration\manager::require_registered();
$inst = instance::get_from_cmid($params['cmid'], $params['groupid']);
self::validate_context($inst->context);
require_capability('mod/meetletacbmarketplace:view', $inst->context);
featureaccess::require_feature(featureaccess::CONTENT, $inst->context, $inst->record);
$service = new service();
$service->login_admin();
$item = content::find_in_meeting($service, $inst->require_meeting(), (int)$params['contentscoid']);
$service->logout();
if (!$item || empty($item->urlpath)) {
throw new moodle_exception('nocontent', 'meetletacbmarketplace');
}
try {
$adoberole = roles::resolve_adobe_role($inst->record, $inst->context, $USER->id);
$permission = $adoberole ?: service::ROLE_PARTICIPANT;
$auth = sso::provision_and_login($USER, (int)$params['contentscoid'], $permission);
$url = config::content_view_url((string)$item->urlpath, $auth['cookie']);
return usererror::ws_ok($url);
} catch (\Throwable $e) {
if ($e instanceof moodle_exception && in_array($e->errorcode, ['nopermissions', 'requirelogin', 'invalidparameter'], true)) {
throw $e;
}
return usererror::ws_fail($e, $inst->context);
}
}
public static function execute_returns(): external_single_structure {
return new external_single_structure(usererror::ws_url_returns());
}
}
+126
View File
@@ -0,0 +1,126 @@
<?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/>.
/**
* Ajax: build SSO join URL.
*
* @package mod_meetletacbmarketplace
* @copyright 2026 MeetLet
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_meetletacbmarketplace\external;
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_single_structure;
use core_external\external_value;
use mod_meetletacbmarketplace\api\service;
use mod_meetletacbmarketplace\local\config;
use mod_meetletacbmarketplace\local\instance;
use mod_meetletacbmarketplace\local\roles;
use mod_meetletacbmarketplace\local\sso;
use mod_meetletacbmarketplace\local\usererror;
use moodle_exception;
defined('MOODLE_INTERNAL') || die();
/**
* Prepare Adobe Connect join URL for current user.
*/
class get_join_url extends external_api {
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters([
'cmid' => new external_value(PARAM_INT, 'Course module id'),
'groupid' => new external_value(PARAM_INT, 'Group id', VALUE_DEFAULT, 0),
]);
}
public static function execute(int $cmid, int $groupid = 0): array {
global $USER;
$params = self::validate_parameters(self::execute_parameters(), [
'cmid' => $cmid,
'groupid' => $groupid,
]);
\mod_meetletacbmarketplace\registration\manager::require_registered();
$inst = instance::get_from_cmid($params['cmid'], $params['groupid']);
self::validate_context($inst->context);
require_capability('mod/meetletacbmarketplace:view', $inst->context);
if (!\mod_meetletacbmarketplace\local\featureaccess::allowed(
\mod_meetletacbmarketplace\local\featureaccess::JOIN,
$inst->context,
$inst->record
)) {
throw new moodle_exception('notparticipant', 'meetletacbmarketplace');
}
if ($inst->cm->groupmode != NOGROUPS) {
$usrgroups = groups_get_user_groups($inst->course->id, $USER->id);
$usrgroups = $usrgroups[0] ?? [];
$aag = has_capability('moodle/site:accessallgroups', $inst->context);
if (!$aag && $params['groupid'] && !in_array($params['groupid'], $usrgroups, false)) {
throw new moodle_exception('usergrouprequired', 'meetletacbmarketplace');
}
}
$adoberole = roles::resolve_adobe_role($inst->record, $inst->context, $USER->id);
if ($adoberole === null) {
throw new moodle_exception('notparticipant', 'meetletacbmarketplace');
}
try {
// Gate join before any Adobe user provisioning / SSO.
$lifecycle = \mod_meetletacbmarketplace\local\meetingstatus::resolve($inst, (int)$USER->id, true);
if (empty($lifecycle['joinable'])) {
$code = !empty($lifecycle['errorcode']) ? $lifecycle['errorcode'] : 'meetingended';
throw new moodle_exception($code, 'meetletacbmarketplace');
}
$scoid = $inst->require_meeting();
$service = new service();
$service->login_admin();
$info = $service->meeting_info($scoid);
$service->logout();
if (!$info) {
throw new moodle_exception('nomeeting', 'meetletacbmarketplace');
}
$auth = sso::provision_and_login($USER, $scoid, $adoberole);
$url = config::with_html_client(config::public_url($info->url, $auth['cookie']));
$event = \mod_meetletacbmarketplace\event\meeting_joined::create([
'context' => $inst->context,
'objectid' => $inst->record->id,
'relateduserid' => $USER->id,
]);
$event->trigger();
return usererror::ws_ok($url);
} catch (\Throwable $e) {
if ($e instanceof moodle_exception && in_array($e->errorcode, ['nopermissions', 'requirelogin', 'invalidparameter'], true)) {
throw $e;
}
return usererror::ws_fail($e, $inst->context);
}
}
public static function execute_returns(): external_single_structure {
return new external_single_structure(usererror::ws_url_returns());
}
}
+86
View File
@@ -0,0 +1,86 @@
<?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/>.
/**
* Ajax: poll meeting join status.
*
* @package mod_meetletacbmarketplace
* @copyright 2026 MeetLet
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_meetletacbmarketplace\external;
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_single_structure;
use core_external\external_value;
use mod_meetletacbmarketplace\local\featureaccess;
use mod_meetletacbmarketplace\local\instance;
use mod_meetletacbmarketplace\local\meetingstatus;
defined('MOODLE_INTERNAL') || die();
/**
* Lightweight status poll for the activity hero badge / join gate.
*/
class get_meeting_status extends external_api {
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters([
'cmid' => new external_value(PARAM_INT, 'Course module id'),
'groupid' => new external_value(PARAM_INT, 'Group id', VALUE_DEFAULT, 0),
]);
}
public static function execute(int $cmid, int $groupid = 0): array {
global $USER;
$params = self::validate_parameters(self::execute_parameters(), [
'cmid' => $cmid,
'groupid' => $groupid,
]);
\mod_meetletacbmarketplace\registration\manager::require_registered();
$inst = instance::get_from_cmid($params['cmid'], $params['groupid']);
self::validate_context($inst->context);
require_capability('mod/meetletacbmarketplace:view', $inst->context);
$info = meetingstatus::resolve($inst, (int)$USER->id, true);
$featurejoin = featureaccess::allowed(featureaccess::JOIN, $inst->context, $inst->record);
$joinable = $featurejoin && !empty($info['joinable']);
return [
'status' => (string)$info['status'],
'statuslabel' => (string)$info['statuslabel'],
'joinable' => (bool)$joinable,
'message' => (string)$info['message'],
'errorcode' => (string)$info['errorcode'],
'servertime' => time(),
];
}
public static function execute_returns(): external_single_structure {
return new external_single_structure([
'status' => new external_value(PARAM_ALPHANUMEXT, 'Status code'),
'statuslabel' => new external_value(PARAM_TEXT, 'Localized status label'),
'joinable' => new external_value(PARAM_BOOL, 'Whether join may be requested'),
'message' => new external_value(PARAM_TEXT, 'User-facing gate message'),
'errorcode' => new external_value(PARAM_ALPHANUMEXT, 'Exception string id when not joinable'),
'servertime' => new external_value(PARAM_INT, 'Server unix time'),
]);
}
}
+117
View File
@@ -0,0 +1,117 @@
<?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/>.
/**
* Ajax: SSO URL to play or edit a recording in Adobe Connect.
*
* @package mod_meetletacbmarketplace
* @copyright 2026 MeetLet <info@meetlet.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_meetletacbmarketplace\external;
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_single_structure;
use core_external\external_value;
use mod_meetletacbmarketplace\api\service;
use mod_meetletacbmarketplace\local\config;
use mod_meetletacbmarketplace\local\featureaccess;
use mod_meetletacbmarketplace\local\instance;
use mod_meetletacbmarketplace\local\roles;
use mod_meetletacbmarketplace\local\sso;
use mod_meetletacbmarketplace\local\usererror;
use moodle_exception;
defined('MOODLE_INTERNAL') || die();
/**
* Prepare Adobe Connect recording play/edit URL with session for current user.
*/
class get_recording_launch_url extends external_api {
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters([
'cmid' => new external_value(PARAM_INT, 'Course module id'),
'recordingscoid' => new external_value(PARAM_INT, 'Recording sco id'),
'mode' => new external_value(PARAM_ALPHA, 'play or edit'),
'groupid' => new external_value(PARAM_INT, 'Group id', VALUE_DEFAULT, 0),
]);
}
public static function execute(int $cmid, int $recordingscoid, string $mode, int $groupid = 0): array {
global $DB, $USER;
$params = self::validate_parameters(self::execute_parameters(), [
'cmid' => $cmid,
'recordingscoid' => $recordingscoid,
'mode' => $mode,
'groupid' => $groupid,
]);
$mode = strtolower($params['mode']);
if (!in_array($mode, ['play', 'edit'], true)) {
throw new moodle_exception('error', 'meetletacbmarketplace');
}
\mod_meetletacbmarketplace\registration\manager::require_registered();
$inst = instance::get_from_cmid($params['cmid'], $params['groupid']);
self::validate_context($inst->context);
require_capability('mod/meetletacbmarketplace:view', $inst->context);
featureaccess::require_feature(featureaccess::RECORDINGS, $inst->context, $inst->record);
if ($mode === 'edit') {
featureaccess::require_feature(featureaccess::ADOBEEDITOR, $inst->context, $inst->record);
}
$record = $DB->get_record('meetlet_marketplace_recordings', [
'instanceid' => $inst->record->id,
'recordingscoid' => $params['recordingscoid'],
]);
if (!$record || empty($record->urlpath)) {
throw new moodle_exception('norecordings', 'meetletacbmarketplace');
}
try {
// Prefer meeting role when available; fall back to view for play / host for edit.
$adoberole = roles::resolve_adobe_role($inst->record, $inst->context, $USER->id);
if ($mode === 'edit') {
$permission = ($adoberole === service::ROLE_HOST || $adoberole === service::ROLE_PRESENTER)
? $adoberole
: service::ROLE_HOST;
} else {
$permission = $adoberole ?: service::ROLE_PARTICIPANT;
}
$auth = sso::provision_and_login($USER, (int)$params['recordingscoid'], $permission);
$url = $mode === 'edit'
? config::recording_editor_url((string)$record->urlpath, $auth['cookie'])
: config::recording_play_url((string)$record->urlpath, $auth['cookie']);
return usererror::ws_ok($url);
} catch (\Throwable $e) {
if ($e instanceof moodle_exception && in_array($e->errorcode, ['nopermissions', 'requirelogin', 'invalidparameter'], true)) {
throw $e;
}
return usererror::ws_fail($e, $inst->context);
}
}
public static function execute_returns(): external_single_structure {
return new external_single_structure(usererror::ws_url_returns());
}
}
+346
View File
@@ -0,0 +1,346 @@
<?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/>.
/**
* Ajax: load tab HTML/data.
*
* @package mod_meetletacbmarketplace
* @copyright 2026 MeetLet
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_meetletacbmarketplace\external;
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_single_structure;
use core_external\external_value;
use mod_meetletacbmarketplace\api\service;
use mod_meetletacbmarketplace\local\config;
use mod_meetletacbmarketplace\local\content;
use mod_meetletacbmarketplace\local\featureaccess;
use mod_meetletacbmarketplace\local\icons;
use mod_meetletacbmarketplace\local\instance;
use mod_meetletacbmarketplace\local\recordings;
use moodle_exception;
defined('MOODLE_INTERNAL') || die();
/**
* Return tab payload for the activity UI.
*/
class get_tab extends external_api {
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters([
'cmid' => new external_value(PARAM_INT, 'Course module id'),
'tab' => new external_value(PARAM_ALPHA, 'Tab name'),
'groupid' => new external_value(PARAM_INT, 'Group id', VALUE_DEFAULT, 0),
]);
}
public static function execute(int $cmid, string $tab, int $groupid = 0): array {
global $PAGE;
$params = self::validate_parameters(self::execute_parameters(), [
'cmid' => $cmid,
'tab' => $tab,
'groupid' => $groupid,
]);
\mod_meetletacbmarketplace\registration\manager::require_registered();
$inst = instance::get_from_cmid($params['cmid'], $params['groupid']);
self::validate_context($inst->context);
require_capability('mod/meetletacbmarketplace:view', $inst->context);
$PAGE->set_context($inst->context);
if (!config::is_configured()) {
return [
'html' => \html_writer::div(get_string('notconfigured', 'meetletacbmarketplace'), 'meetlet-alert'),
'tab' => $params['tab'],
];
}
$html = '';
switch ($params['tab']) {
case 'details':
// Served locally in the activity page; keep endpoint defensive.
$html = \html_writer::div(get_string('emptydetails', 'meetletacbmarketplace'), 'meetlet-empty');
break;
case 'info':
// Legacy tab id — no Adobe call.
$html = \html_writer::div(get_string('emptydetails', 'meetletacbmarketplace'), 'meetlet-empty');
break;
case 'recordings':
featureaccess::require_feature(featureaccess::RECORDINGS, $inst->context, $inst->record);
$html = self::render_recordings($inst);
break;
case 'content':
featureaccess::require_feature(featureaccess::CONTENT, $inst->context, $inst->record);
$html = self::render_content($inst);
break;
default:
throw new moodle_exception('error', 'meetletacbmarketplace');
}
return ['html' => $html, 'tab' => $params['tab']];
}
public static function execute_returns(): external_single_structure {
return new external_single_structure([
'html' => new external_value(PARAM_RAW, 'Tab HTML'),
'tab' => new external_value(PARAM_ALPHA, 'Tab id'),
]);
}
/**
* Toolbar with refresh for cacheable Adobe tabs.
*/
protected static function wrap_refreshable(string $html, string $tab, bool $pending = false): string {
$refreshlabel = get_string('refreshtab', 'meetletacbmarketplace');
$refreshcontent = icons::svg('refresh')
. \html_writer::span($refreshlabel, 'meetlet-btn-with-icon__label');
$bits = [
\html_writer::tag('button', $refreshcontent, [
'type' => 'button',
'class' => 'meetlet-btn meetlet-btn--ghost meetlet-btn--sm meetlet-btn-with-icon meetlet-tabtoolbar__refresh',
'data-action' => 'refresh-tab',
'data-tab' => $tab,
'title' => $refreshlabel,
'aria-label' => $refreshlabel,
]),
];
if ($pending) {
$bits[] = \html_writer::div(
\html_writer::span(get_string('jobpollwait', 'meetletacbmarketplace'), 'meetlet-jobpoll__label') .
\html_writer::div(\html_writer::div('', 'meetlet-jobpoll__bar'), 'meetlet-jobpoll__track'),
'meetlet-jobpoll',
['aria-live' => 'polite']
);
}
$bar = \html_writer::div(implode('', $bits), 'meetlet-tabtoolbar');
$attrs = ['data-meetlet-tab' => $tab];
if ($pending) {
$attrs['data-meetlet-jobs-pending'] = '1';
$attrs['data-meetlet-jobs-poll-ms'] = '60000';
}
return \html_writer::div($bar . $html, 'meetlet-tabbody', $attrs);
}
/**
* Toolbar control with icon and visible label (matches refresh button styling).
*
* @param array $attrs Extra attributes (href makes it a link).
*/
protected static function labeled_action(string $icon, string $label, array $attrs = []): string {
$extraclass = $attrs['class'] ?? '';
unset($attrs['class']);
$classes = trim(
'meetlet-btn meetlet-btn--ghost meetlet-btn--sm meetlet-btn-with-icon ' . $extraclass
);
$attrs['class'] = $classes;
$attrs['title'] = $label;
$attrs['aria-label'] = $label;
$content = icons::svg($icon)
. \html_writer::span($label, 'meetlet-btn-with-icon__label');
if (!empty($attrs['href'])) {
$url = $attrs['href'];
unset($attrs['href']);
$attrs['target'] = $attrs['target'] ?? '_blank';
if (($attrs['target'] ?? '') === '_self') {
unset($attrs['rel']);
} else {
$attrs['rel'] = $attrs['rel'] ?? 'noopener';
}
return \html_writer::link($url, $content, $attrs);
}
$attrs['type'] = $attrs['type'] ?? 'button';
return \html_writer::tag('button', $content, $attrs);
}
/**
* Icon control with hover tip.
*
* @param array $attrs Extra attributes (href makes it a link).
*/
protected static function icon_action(string $icon, string $label, array $attrs = []): string {
$classes = trim('meetlet-iconbtn ' . ($attrs['class'] ?? ''));
unset($attrs['class']);
$attrs['class'] = $classes;
$attrs['data-tip'] = $label;
$attrs['aria-label'] = $label;
$content = icons::svg($icon) . \html_writer::span($label, 'meetlet-sr-only');
if (!empty($attrs['href'])) {
$url = $attrs['href'];
unset($attrs['href']);
$attrs['target'] = $attrs['target'] ?? '_blank';
if (($attrs['target'] ?? '') === '_self') {
unset($attrs['rel']);
} else {
$attrs['rel'] = $attrs['rel'] ?? 'noopener';
}
return \html_writer::link($url, $content, $attrs);
}
$attrs['type'] = $attrs['type'] ?? 'button';
return \html_writer::tag('button', $content, $attrs);
}
protected static function format_jobstatus(string $status, bool $mp4ready = false): string {
return recordings::jobstatus_label($status, $mp4ready);
}
protected static function render_recordings(instance $inst): string {
$rows = recordings::for_tab($inst);
if (empty($rows)) {
return self::wrap_refreshable(
\html_writer::div(get_string('norecordings', 'meetletacbmarketplace'), 'meetlet-empty'),
'recordings'
);
}
$canedit = featureaccess::allowed(featureaccess::EDITRECORDING, $inst->context, $inst->record);
$canadobeedit = featureaccess::allowed(featureaccess::ADOBEEDITOR, $inst->context, $inst->record);
$table = new \html_table();
$table->attributes['class'] = 'generaltable meetlet-table meetlet-table--recordings';
$table->head = [
get_string('recordingname', 'meetletacbmarketplace'),
get_string('recordingdate', 'meetletacbmarketplace'),
get_string('duration', 'meetletacbmarketplace'),
get_string('sessiondetails', 'meetletacbmarketplace'),
];
foreach ($rows as $r) {
$actions = [];
$actions[] = self::icon_action('play', get_string('play', 'meetletacbmarketplace'), [
'class' => 'meetlet-iconbtn--play',
'data-action' => 'launch-recording',
'data-mode' => 'play',
'data-recording' => $r->recordingscoid,
]);
if ($canadobeedit && !empty($r->urlpath)) {
$actions[] = self::icon_action('adobeedit', get_string('adobeeditor', 'meetletacbmarketplace'), [
'class' => 'meetlet-iconbtn--adobeedit',
'data-action' => 'launch-recording',
'data-mode' => 'edit',
'data-recording' => $r->recordingscoid,
]);
}
$namehtml = self::render_recording_name((string)$r->name, (int)$r->recordingscoid, $canedit);
$dur = $r->duration ? gmdate('H:i:s', (int)$r->duration) : '-';
$recdate = !empty($r->timestart) ? userdate((int)$r->timestart) : '—';
$table->data[] = [
$namehtml,
$recdate,
$dur,
\html_writer::div(implode('', $actions), 'meetlet-iconactions'),
];
}
return self::wrap_refreshable(\html_writer::div(\html_writer::table($table), 'meetlet-card'), 'recordings', false);
}
/**
* Recording title with optional inline rename control.
*/
protected static function render_recording_name(string $name, int $recordingscoid, bool $canedit): string {
$text = \html_writer::span(format_string($name), 'meetlet-recname__text');
if (!$canedit) {
return \html_writer::div($text, 'meetlet-recname');
}
$pencil = self::icon_action('edit', get_string('editrecording', 'meetletacbmarketplace'), [
'class' => 'meetlet-iconbtn--edit meetlet-iconbtn--inline',
'data-action' => 'inline-rename',
]);
$input = \html_writer::empty_tag('input', [
'type' => 'text',
'class' => 'meetlet-recname__input',
'value' => $name,
'maxlength' => 255,
'hidden' => 'hidden',
]);
return \html_writer::div($text . $pencil . $input, 'meetlet-recname', [
'data-recording' => $recordingscoid,
'data-name' => $name,
]);
}
protected static function render_content(instance $inst): string {
global $USER;
$service = new service();
$service->login_admin();
$items = $service->list_meeting_content($inst->require_meeting());
$service->logout();
if (empty($items)) {
return self::wrap_refreshable(
\html_writer::div(get_string('nocontent', 'meetletacbmarketplace'), 'meetlet-empty'),
'content'
);
}
$table = new \html_table();
$table->attributes['class'] = 'generaltable meetlet-table meetlet-table--content';
$table->head = [
get_string('name'),
get_string('contenttype', 'meetletacbmarketplace'),
get_string('contentsize', 'meetletacbmarketplace'),
get_string('contentdate', 'meetletacbmarketplace'),
get_string('sessiondetails', 'meetletacbmarketplace'),
];
foreach ($items as $item) {
$scoid = (int)($item->scoid ?? 0);
$actions = [
self::icon_action('play', get_string('view'), [
'class' => 'meetlet-iconbtn--play',
'data-action' => 'launch-content',
'data-content' => $scoid,
]),
];
if ($scoid > 0 && !empty($item->name)) {
$dlurl = content::download_url((int)$USER->id, (int)$inst->cm->id, $scoid)->out(false);
$actions[] = self::icon_action('download', get_string('downloadcontent', 'meetletacbmarketplace'), [
'href' => $dlurl,
'class' => 'meetlet-iconbtn--download',
'target' => '_self',
'download' => $item->name,
'data-action' => 'download-content',
]);
}
$table->data[] = [
format_string($item->name),
s($item->typelabel ?? service::content_type_label(
(string)($item->icon ?? ''),
(string)($item->type ?? ''),
(string)($item->name ?? '')
)),
self::format_bytes((int)($item->bytesize ?? 0)),
s($item->datecreated_fmt !== '' ? $item->datecreated_fmt : '—'),
\html_writer::div(implode('', $actions), 'meetlet-iconactions'),
];
}
return self::wrap_refreshable(\html_writer::div(\html_writer::table($table), 'meetlet-card'), 'content');
}
protected static function format_bytes(int $bytes): string {
if ($bytes <= 0) {
return '—';
}
$units = ['B', 'KB', 'MB', 'GB'];
$i = 0;
$n = (float)$bytes;
while ($n >= 1024 && $i < count($units) - 1) {
$n /= 1024;
$i++;
}
return format_float($n, $i === 0 ? 0 : 1) . ' ' . $units[$i];
}
}
+108
View File
@@ -0,0 +1,108 @@
<?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/>.
/**
* Ajax: rename a recording SCO.
*
* @package mod_meetletacbmarketplace
* @copyright 2026 MeetLet
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_meetletacbmarketplace\external;
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_single_structure;
use core_external\external_value;
use mod_meetletacbmarketplace\api\service;
use mod_meetletacbmarketplace\local\instance;
use moodle_exception;
defined('MOODLE_INTERNAL') || die();
/**
* Rename Adobe Connect recording.
*/
class rename_recording extends external_api {
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters([
'cmid' => new external_value(PARAM_INT, 'Course module id'),
'recordingscoid' => new external_value(PARAM_INT, 'Recording sco id'),
'name' => new external_value(PARAM_TEXT, 'New recording name'),
'groupid' => new external_value(PARAM_INT, 'Group id', VALUE_DEFAULT, 0),
]);
}
public static function execute(int $cmid, int $recordingscoid, string $name, int $groupid = 0): array {
global $DB;
$params = self::validate_parameters(self::execute_parameters(), [
'cmid' => $cmid,
'recordingscoid' => $recordingscoid,
'name' => $name,
'groupid' => $groupid,
]);
\mod_meetletacbmarketplace\registration\manager::require_registered();
$inst = instance::get_from_cmid($params['cmid'], $params['groupid']);
self::validate_context($inst->context);
require_capability('mod/meetletacbmarketplace:view', $inst->context);
\mod_meetletacbmarketplace\local\featureaccess::require_feature(
\mod_meetletacbmarketplace\local\featureaccess::EDITRECORDING,
$inst->context,
$inst->record
);
$name = trim($params['name']);
if ($name === '') {
throw new moodle_exception('error', 'meetletacbmarketplace');
}
$service = new service();
$service->login_admin();
$ok = $service->rename_sco((int)$params['recordingscoid'], $name);
$service->logout();
if (!$ok) {
throw new moodle_exception('errorrenamerecording', 'meetletacbmarketplace');
}
$record = $DB->get_record('meetlet_marketplace_recordings', [
'instanceid' => $inst->record->id,
'recordingscoid' => (int)$params['recordingscoid'],
]);
if ($record) {
$record->name = $name;
$record->timemodified = time();
$DB->update_record('meetlet_marketplace_recordings', $record);
}
return [
'success' => true,
'name' => $name,
'message' => get_string('recordingrenamed', 'meetletacbmarketplace'),
];
}
public static function execute_returns(): external_single_structure {
return new external_single_structure([
'success' => new external_value(PARAM_BOOL, 'OK'),
'name' => new external_value(PARAM_TEXT, 'New name'),
'message' => new external_value(PARAM_TEXT, 'Message'),
]);
}
}
+110
View File
@@ -0,0 +1,110 @@
<?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/>.
/**
* Ajax: search/list existing Adobe meetings for the mod form.
*
* @package mod_meetletacbmarketplace
* @copyright 2026 MeetLet
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_meetletacbmarketplace\external;
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_multiple_structure;
use core_external\external_single_structure;
use core_external\external_value;
use mod_meetletacbmarketplace\api\service;
use mod_meetletacbmarketplace\local\config;
use moodle_exception;
defined('MOODLE_INTERNAL') || die();
/**
* Return a short list of server meetings (latest / search).
*/
class search_server_meetings extends external_api {
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters([
'query' => new external_value(PARAM_TEXT, 'Search query', VALUE_DEFAULT, ''),
'limit' => new external_value(PARAM_INT, 'Max results', VALUE_DEFAULT, 10),
'courseid' => new external_value(PARAM_INT, 'Course id for capability check', VALUE_DEFAULT, 0),
]);
}
public static function execute(string $query = '', int $limit = 10, int $courseid = 0): array {
$params = self::validate_parameters(self::execute_parameters(), [
'query' => $query,
'limit' => $limit,
'courseid' => $courseid,
]);
\mod_meetletacbmarketplace\registration\manager::require_registered();
if (!empty($params['courseid'])) {
$context = \context_course::instance((int)$params['courseid']);
} else {
$context = \context_system::instance();
}
self::validate_context($context);
if (!has_capability('moodle/course:manageactivities', $context)
&& !has_capability('mod/meetletacbmarketplace:addinstance', $context)) {
throw new moodle_exception('nopermissions', 'error', '', 'search meetings');
}
if (!config::is_configured()) {
throw new moodle_exception('notconfigured', 'meetletacbmarketplace');
}
$limit = max(1, min(50, (int)$params['limit']));
$service = new service();
$service->login_admin();
$rows = $service->list_server_meetings((string)$params['query'], $limit);
$service->logout();
$out = [];
foreach ($rows as $m) {
$label = $m->name;
if (!empty($m->urlpath)) {
$label .= ' (' . $m->urlpath . ')';
}
$out[] = [
'scoid' => (int)$m->scoid,
'name' => (string)$m->name,
'label' => $label,
'urlpath' => trim((string)$m->urlpath, '/'),
'starttime' => !empty($m->datebegin) ? (int)strtotime((string)$m->datebegin) : 0,
'endtime' => !empty($m->dateend) ? (int)strtotime((string)$m->dateend) : 0,
];
}
return ['meetings' => $out];
}
public static function execute_returns(): external_single_structure {
return new external_single_structure([
'meetings' => new external_multiple_structure(new external_single_structure([
'scoid' => new external_value(PARAM_INT, 'SCO id'),
'name' => new external_value(PARAM_TEXT, 'Meeting name'),
'label' => new external_value(PARAM_TEXT, 'Select label'),
'urlpath' => new external_value(PARAM_TEXT, 'URL path'),
'starttime' => new external_value(PARAM_INT, 'Start unix time'),
'endtime' => new external_value(PARAM_INT, 'End unix time'),
])),
]);
}
}