Adobe Connect Bridge free edition (meetletacbfree) v1.1.3
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,655 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Plugin config helpers.
|
||||
*
|
||||
* @package mod_meetletacbfree
|
||||
* @copyright 2026 MeetLet
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace mod_meetletacbfree\local;
|
||||
|
||||
/**
|
||||
* Read MeetLet / Adobe Connect global settings.
|
||||
*/
|
||||
class config {
|
||||
/** Activity follows site-wide setting. */
|
||||
public const OVERRIDE_INHERIT = 0;
|
||||
/** Activity explicitly disables the setting. */
|
||||
public const OVERRIDE_OFF = 1;
|
||||
/** Activity explicitly enables the setting. */
|
||||
public const OVERRIDE_ON = 2;
|
||||
/**
|
||||
* API endpoint URL (may already include https://.../api/xml).
|
||||
*/
|
||||
public static function get_api_url(): string {
|
||||
return trim((string)get_config('meetletacbfree', 'host'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Meeting host domain (no protocol).
|
||||
*/
|
||||
public static function get_meet_host(): string {
|
||||
return trim((string)get_config('meetletacbfree', 'meethost'));
|
||||
}
|
||||
|
||||
public static function get_port(): int {
|
||||
$port = (int)get_config('meetletacbfree', 'port');
|
||||
return $port > 0 ? $port : 443;
|
||||
}
|
||||
|
||||
public static function use_https(): bool {
|
||||
return (bool)get_config('meetletacbfree', 'https');
|
||||
}
|
||||
|
||||
public static function get_admin_login(): string {
|
||||
return (string)get_config('meetletacbfree', 'admin_login');
|
||||
}
|
||||
|
||||
public static function get_admin_password(): string {
|
||||
return (string)get_config('meetletacbfree', 'admin_password');
|
||||
}
|
||||
|
||||
public static function get_http_auth_header(): string {
|
||||
$h = trim((string)get_config('meetletacbfree', 'admin_httpauth'));
|
||||
return $h !== '' ? $h : 'Generate-Safe-Random-Token';
|
||||
}
|
||||
|
||||
/**
|
||||
* When true, also look up legacy Connect accounts by email/username
|
||||
* and realign them to mlu{id}. SSO identity itself is always Moodle id.
|
||||
*/
|
||||
public static function use_email_login(): bool {
|
||||
return (bool)get_config('meetletacbfree', 'email_login');
|
||||
}
|
||||
|
||||
/**
|
||||
* Site-wide auto MP4 after Adobe Connect class actually ends (room empty).
|
||||
* Per-activity checkbox is an extra opt-in when this is off.
|
||||
*/
|
||||
public static function auto_offline_default(): bool {
|
||||
if (!self::offline_render_active()) {
|
||||
return false;
|
||||
}
|
||||
return (bool)get_config('meetletacbfree', 'auto_offline');
|
||||
}
|
||||
|
||||
/**
|
||||
* Minutes between recording-sync / auto-MP4 cron runs (1–60, default 5).
|
||||
*/
|
||||
public static function cron_interval_minutes(): int {
|
||||
$v = (int)get_config('meetletacbfree', 'cron_interval');
|
||||
if ($v < 1) {
|
||||
$v = 5;
|
||||
}
|
||||
if ($v > 60) {
|
||||
$v = 60;
|
||||
}
|
||||
return $v;
|
||||
}
|
||||
|
||||
/** Send collected jobs with every MeetLet cron run. */
|
||||
public const DISPATCH_IMMEDIATE = 'immediate';
|
||||
/** Send collected jobs only during selected site-timezone hours. */
|
||||
public const DISPATCH_WINDOW = 'window';
|
||||
/** Default overnight hours (22:00–05:59). */
|
||||
public const DEFAULT_DISPATCH_HOURS = '22,23,0,1,2,3,4,5';
|
||||
|
||||
/**
|
||||
* How auto-MP4 jobs are handed to the Agent.
|
||||
*/
|
||||
public static function agent_dispatch_mode(): string {
|
||||
$v = trim((string)get_config('meetletacbfree', 'agent_dispatch'));
|
||||
return $v === self::DISPATCH_WINDOW ? self::DISPATCH_WINDOW : self::DISPATCH_IMMEDIATE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selected hours (0–23) in the site timezone. Empty means “none”.
|
||||
*
|
||||
* @return int[]
|
||||
*/
|
||||
public static function agent_dispatch_hours(): array {
|
||||
$raw = (string)get_config('meetletacbfree', 'agent_dispatch_hours');
|
||||
if ($raw === '') {
|
||||
$raw = self::DEFAULT_DISPATCH_HOURS;
|
||||
}
|
||||
return self::parse_dispatch_hours($raw);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int[]
|
||||
*/
|
||||
public static function parse_dispatch_hours(string $raw): array {
|
||||
$hours = [];
|
||||
foreach (preg_split('/[,\s]+/', $raw, -1, PREG_SPLIT_NO_EMPTY) as $bit) {
|
||||
if (!preg_match('/^\d{1,2}$/', $bit)) {
|
||||
continue;
|
||||
}
|
||||
$h = (int)$bit;
|
||||
if ($h >= 0 && $h <= 23) {
|
||||
$hours[$h] = $h;
|
||||
}
|
||||
}
|
||||
$hours = array_values($hours);
|
||||
sort($hours);
|
||||
return $hours;
|
||||
}
|
||||
|
||||
/**
|
||||
* Current hour 0–23 in the Moodle server / site timezone.
|
||||
*/
|
||||
public static function site_hour_now(): int {
|
||||
$tz = \core_date::get_server_timezone_object();
|
||||
return (int)(new \DateTimeImmutable('now', $tz))->format('G');
|
||||
}
|
||||
|
||||
/**
|
||||
* True when collected jobs may be POSTed to the Agent.
|
||||
*/
|
||||
public static function agent_send_allowed_now(): bool {
|
||||
if (self::agent_dispatch_mode() !== self::DISPATCH_WINDOW) {
|
||||
return true;
|
||||
}
|
||||
$hours = self::agent_dispatch_hours();
|
||||
if (!$hours) {
|
||||
return false;
|
||||
}
|
||||
return in_array(self::site_hour_now(), $hours, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unix time of the next allowed send hour start, or 0 if sending is allowed now / immediate.
|
||||
*/
|
||||
public static function agent_send_next_open(): int {
|
||||
if (self::agent_send_allowed_now()) {
|
||||
return 0;
|
||||
}
|
||||
$hours = self::agent_dispatch_hours();
|
||||
if (!$hours) {
|
||||
return 0;
|
||||
}
|
||||
$tz = \core_date::get_server_timezone_object();
|
||||
$now = new \DateTimeImmutable('now', $tz);
|
||||
$cur = (int)$now->format('G');
|
||||
foreach ($hours as $h) {
|
||||
if ($h > $cur) {
|
||||
return $now->setTime($h, 0, 0)->getTimestamp();
|
||||
}
|
||||
}
|
||||
return $now->modify('+1 day')->setTime($hours[0], 0, 0)->getTimestamp();
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the scheduled-task minute field from plugin settings.
|
||||
*/
|
||||
public static function apply_cron_interval(): void {
|
||||
global $DB;
|
||||
$mins = self::cron_interval_minutes();
|
||||
$row = $DB->get_record('task_scheduled', ['classname' => '\\mod_meetletacbfree\\task\\sync_recordings']);
|
||||
if (!$row) {
|
||||
return;
|
||||
}
|
||||
$row->minute = $mins <= 1 ? '*' : ('*/' . $mins);
|
||||
$row->nextruntime = time() + ($mins * MINSECS);
|
||||
$DB->update_record('task_scheduled', $row);
|
||||
}
|
||||
|
||||
/**
|
||||
* Offline MP4 is Premium-only: paid license + mp4_offline entitlement.
|
||||
* Free / unlicensed sites never activate Agent auto/manual MP4 paths.
|
||||
*/
|
||||
public static function offline_render_active(): bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configured Render Agent / offline listener base URL (may be empty).
|
||||
*/
|
||||
public static function agent_url(): string {
|
||||
return trim((string)get_config('meetletacbfree', 'agent_url'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Site-wide default meeting UI language (Adobe Connect), empty = server default.
|
||||
*/
|
||||
public static function default_meeting_lang(): string {
|
||||
$v = get_config('meetletacbfree', 'default_meetinglang');
|
||||
if ($v === false || $v === null) {
|
||||
return '';
|
||||
}
|
||||
return clean_param((string)$v, PARAM_ALPHANUMEXT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Site-wide default: block participants until host joins.
|
||||
*/
|
||||
public static function default_wait_for_host(): bool {
|
||||
return (bool)get_config('meetletacbfree', 'default_waitforhost');
|
||||
}
|
||||
|
||||
/**
|
||||
* Language choices for meeting UI (site + activity forms).
|
||||
*
|
||||
* @return array<string,string> code => label
|
||||
*/
|
||||
public static function meeting_lang_options(): array {
|
||||
return [
|
||||
'' => get_string('meetinglang_default', 'meetletacbfree'),
|
||||
'en' => 'English',
|
||||
'fa' => 'فارسی',
|
||||
'ar' => 'العربية',
|
||||
'de' => 'Deutsch',
|
||||
'fr' => 'Français',
|
||||
'es' => 'Español',
|
||||
'tr' => 'Türkçe',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the manual “request offline video” button is allowed (optionally per activity).
|
||||
*
|
||||
* @param \stdClass|null $activity
|
||||
*/
|
||||
public static function allow_manual_offline(?\stdClass $activity = null): bool {
|
||||
if (!self::offline_render_active()) {
|
||||
return false;
|
||||
}
|
||||
return self::show_feature('makeoffline', $activity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a UI feature is enabled globally.
|
||||
*
|
||||
* @param string $feature zip|offlineplayer|recordingstab|contenttab|mp4|makeoffline
|
||||
*/
|
||||
public static function feature_enabled(string $feature): bool {
|
||||
// Offline player (pbMode) removed from product — always hidden.
|
||||
if ($feature === 'offlineplayer') {
|
||||
return false;
|
||||
}
|
||||
if (($feature === 'mp4' || $feature === 'makeoffline') && !self::offline_render_active()) {
|
||||
return false;
|
||||
}
|
||||
$key = 'enable_' . $feature;
|
||||
$v = get_config('meetletacbfree', $key);
|
||||
if ($v === false && $feature === 'mp4') {
|
||||
$legacy = get_config('meetletacbfree', 'allow_manual_offline');
|
||||
return $legacy === false ? true : (bool)$legacy;
|
||||
}
|
||||
// ZIP is opt-in; other features default on when unset.
|
||||
if ($v === false) {
|
||||
return $feature !== 'zip';
|
||||
}
|
||||
return (bool)$v;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether teachers may override a feature per activity.
|
||||
*
|
||||
* @param string $feature zip|offlineplayer|recordingstab|contenttab|mp4|makeoffline
|
||||
*/
|
||||
public static function feature_allow_override(string $feature): bool {
|
||||
$key = 'allow_override_' . $feature;
|
||||
$v = get_config('meetletacbfree', $key);
|
||||
return $v === false ? true : (bool)$v;
|
||||
}
|
||||
|
||||
/**
|
||||
* Effective visibility for a feature on an activity instance (site + activity switches only).
|
||||
* Always combine with featureaccess::allowed() / the role matrix for user actions.
|
||||
*
|
||||
* @param string $feature zip|offlineplayer|recordingstab|contenttab|mp4|makeoffline
|
||||
* @param \stdClass|null $activity
|
||||
*/
|
||||
public static function show_feature(string $feature, ?\stdClass $activity = null): bool {
|
||||
if (!self::feature_enabled($feature)) {
|
||||
return false;
|
||||
}
|
||||
if (!self::feature_allow_override($feature) || $activity === null) {
|
||||
return true;
|
||||
}
|
||||
$fieldmap = [
|
||||
'zip' => 'showzip',
|
||||
'offlineplayer' => 'showofflineplayer',
|
||||
'recordingstab' => 'showrecordingstab',
|
||||
'contenttab' => 'showcontenttab',
|
||||
'mp4' => 'showmp4',
|
||||
'makeoffline' => 'showmakeoffline',
|
||||
];
|
||||
$field = $fieldmap[$feature] ?? null;
|
||||
if ($field === null) {
|
||||
return true;
|
||||
}
|
||||
$siteenabled = true;
|
||||
if ($field === 'showofflineplayer') {
|
||||
$siteenabled = false;
|
||||
}
|
||||
return self::resolve_activity_toggle($activity, $field, $siteenabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Effective wait-for-host value for one activity.
|
||||
*/
|
||||
public static function wait_for_host(?\stdClass $activity = null): bool {
|
||||
$sitevalue = self::default_wait_for_host();
|
||||
return self::resolve_activity_toggle($activity, 'waitforhost', $sitevalue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Effective automatic offline-video value for one activity.
|
||||
*/
|
||||
public static function auto_offline_enabled(?\stdClass $activity = null): bool {
|
||||
if (!self::offline_render_active()) {
|
||||
return false;
|
||||
}
|
||||
return self::resolve_activity_toggle($activity, 'autooffline', self::auto_offline_default());
|
||||
}
|
||||
|
||||
/**
|
||||
* Three-state options for inherit / off / on controls.
|
||||
*
|
||||
* @return array<int,string>
|
||||
*/
|
||||
public static function override_state_options(): array {
|
||||
return [
|
||||
self::OVERRIDE_INHERIT => get_string('settingstate_inherit', 'meetletacbfree'),
|
||||
self::OVERRIDE_OFF => get_string('settingstate_disable', 'meetletacbfree'),
|
||||
self::OVERRIDE_ON => get_string('settingstate_enable', 'meetletacbfree'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Read JSON override map stored on the activity row.
|
||||
*
|
||||
* @return array<string,int>
|
||||
*/
|
||||
public static function activity_setting_overrides(?\stdClass $activity): array {
|
||||
if (!$activity || empty($activity->settingoverrides)) {
|
||||
return [];
|
||||
}
|
||||
$decoded = json_decode((string)$activity->settingoverrides, true);
|
||||
if (!is_array($decoded)) {
|
||||
return [];
|
||||
}
|
||||
$out = [];
|
||||
foreach ($decoded as $key => $value) {
|
||||
$state = self::normalize_override_state($value);
|
||||
if ($state !== self::OVERRIDE_INHERIT) {
|
||||
$out[(string)$key] = $state;
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Effective state for one activity toggle.
|
||||
*/
|
||||
public static function activity_override_state(?\stdClass $activity, string $field): int {
|
||||
$overrides = self::activity_setting_overrides($activity);
|
||||
if (array_key_exists($field, $overrides)) {
|
||||
return $overrides[$field];
|
||||
}
|
||||
return self::OVERRIDE_INHERIT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize one override state from DB / form values.
|
||||
*/
|
||||
public static function normalize_override_state($value): int {
|
||||
$value = (int)$value;
|
||||
if (in_array($value, [self::OVERRIDE_INHERIT, self::OVERRIDE_OFF, self::OVERRIDE_ON], true)) {
|
||||
return $value;
|
||||
}
|
||||
return self::OVERRIDE_INHERIT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build normalized JSON for activity setting overrides.
|
||||
*
|
||||
* @param array<string,mixed> $states
|
||||
*/
|
||||
public static function setting_overrides_json(array $states): string {
|
||||
$out = [];
|
||||
foreach ($states as $field => $value) {
|
||||
$state = self::normalize_override_state($value);
|
||||
if ($state !== self::OVERRIDE_INHERIT) {
|
||||
$out[(string)$field] = $state;
|
||||
}
|
||||
}
|
||||
return $out ? json_encode($out, JSON_UNESCAPED_UNICODE) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a boolean site/activity setting.
|
||||
*/
|
||||
protected static function resolve_activity_toggle(?\stdClass $activity, string $field, bool $sitevalue): bool {
|
||||
if ($activity === null) {
|
||||
return $sitevalue;
|
||||
}
|
||||
$state = self::activity_override_state($activity, $field);
|
||||
if ($state === self::OVERRIDE_OFF) {
|
||||
return false;
|
||||
}
|
||||
if ($state === self::OVERRIDE_ON) {
|
||||
return true;
|
||||
}
|
||||
if (property_exists($activity, 'settingoverrides') || property_exists($activity, 'participantsmode')) {
|
||||
return $sitevalue;
|
||||
}
|
||||
if (!isset($activity->{$field})) {
|
||||
return $sitevalue;
|
||||
}
|
||||
return !empty($activity->{$field});
|
||||
}
|
||||
|
||||
/**
|
||||
* ZIP download link: site/activity “show ZIP” AND role matrix.
|
||||
* Prefer featureaccess::allowed(ZIP) in new code.
|
||||
*/
|
||||
public static function user_can_download_zip(\context $context, ?\stdClass $activity = null, ?int $userid = null): bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether an email is acceptable for Adobe Connect principal-update.
|
||||
*/
|
||||
public static function email_usable_for_adobe(string $email): bool {
|
||||
$email = trim($email);
|
||||
if ($email === '' || strpos($email, '@') === false) {
|
||||
return false;
|
||||
}
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
return false;
|
||||
}
|
||||
// Adobe rejects fake/reserved domains (e.g. test@test.test → invalid/format).
|
||||
if (preg_match('/\.(test|invalid|localhost|local|example)$/i', $email)) {
|
||||
return false;
|
||||
}
|
||||
if (preg_match('/@(users\.moodle\.local|example\.(com|org|net))$/i', $email)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Synthetic Connect email on the meeting host domain.
|
||||
*/
|
||||
public static function synthetic_connect_email(\stdClass $user): string {
|
||||
$host = self::get_meet_host();
|
||||
$host = preg_replace('#^https?://#i', '', $host);
|
||||
$host = explode('/', $host)[0] ?? '';
|
||||
$host = explode(':', $host)[0] ?? '';
|
||||
$host = trim((string)$host);
|
||||
if ($host === '' || !preg_match('/\./', $host)) {
|
||||
global $CFG;
|
||||
$www = parse_url($CFG->wwwroot ?? '', PHP_URL_HOST);
|
||||
$host = is_string($www) && $www !== '' ? $www : 'meetlet.local';
|
||||
}
|
||||
return 'moodleuser' . (int)($user->id ?? 0) . '@' . $host;
|
||||
}
|
||||
|
||||
/**
|
||||
* Canonical Adobe Connect login for a Moodle user.
|
||||
*
|
||||
* Stable across email/username changes: mlu{moodleuserid}.
|
||||
* Header SSO must send this same string (or the principal's current login
|
||||
* until a legacy account is realigned).
|
||||
*/
|
||||
public static function connect_login(\stdClass $user): string {
|
||||
$id = (int)($user->id ?? 0);
|
||||
if ($id > 0) {
|
||||
return 'mlu' . $id;
|
||||
}
|
||||
// Disposable / connection-test users may have no Moodle id.
|
||||
$username = trim((string)($user->username ?? ''));
|
||||
$safe = preg_replace('/[^a-zA-Z0-9._-]/', '', $username);
|
||||
if ($safe !== '') {
|
||||
return 'mlu_' . $safe;
|
||||
}
|
||||
return 'mlu_tmp' . substr(sha1(($user->email ?? '') . '|' . microtime(true)), 0, 12);
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy Connect logins that may already exist for this Moodle user
|
||||
* (before stable mlu{id} identity). Used only for lookup/migration.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public static function legacy_connect_logins(\stdClass $user): array {
|
||||
$out = [];
|
||||
$email = trim((string)($user->email ?? ''));
|
||||
$username = trim((string)($user->username ?? ''));
|
||||
foreach ([
|
||||
self::synthetic_connect_email($user),
|
||||
'moodleuser' . (int)($user->id ?? 0),
|
||||
self::email_usable_for_adobe($email) ? $email : '',
|
||||
($username !== '' && strpos($username, '@') === false) ? $username : '',
|
||||
self::email_usable_for_adobe($username) ? $username : '',
|
||||
] as $candidate) {
|
||||
$candidate = trim((string)$candidate);
|
||||
if ($candidate === '') {
|
||||
continue;
|
||||
}
|
||||
if (strpos($candidate, '@') !== false && !self::email_usable_for_adobe($candidate)) {
|
||||
continue;
|
||||
}
|
||||
if (!in_array($candidate, $out, true)) {
|
||||
$out[] = $candidate;
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Best-effort email for Adobe principal-update (profile field only; not SSO key).
|
||||
*/
|
||||
public static function connect_email(\stdClass $user): string {
|
||||
$email = trim((string)($user->email ?? ''));
|
||||
if (self::email_usable_for_adobe($email)) {
|
||||
return $email;
|
||||
}
|
||||
return self::synthetic_connect_email($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build public meeting/content URL.
|
||||
*/
|
||||
public static function public_url(string $urlpath, string $session = ''): string {
|
||||
$protocol = self::use_https() ? 'https://' : 'http://';
|
||||
$host = self::get_meet_host();
|
||||
$port = self::get_port();
|
||||
$portpart = '';
|
||||
if ($port && $port != 80 && !(self::use_https() && $port == 443)) {
|
||||
$portpart = ':' . $port;
|
||||
}
|
||||
if ($urlpath !== '' && $urlpath[0] !== '/') {
|
||||
$urlpath = '/' . $urlpath;
|
||||
}
|
||||
$url = $protocol . $host . $portpart . $urlpath;
|
||||
if ($session !== '') {
|
||||
$url .= (strpos($url, '?') === false ? '?' : '&') . 'session=' . rawurlencode($session);
|
||||
}
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append query params without duplicating keys.
|
||||
*
|
||||
* @param array<string,string|int|bool> $params
|
||||
*/
|
||||
public static function append_query(string $url, array $params): string {
|
||||
foreach ($params as $key => $value) {
|
||||
if ($value === null || $value === '') {
|
||||
continue;
|
||||
}
|
||||
$pair = rawurlencode((string)$key) . '=' . rawurlencode((string)$value);
|
||||
// Skip if already present.
|
||||
if (preg_match('/([?&])' . preg_quote((string)$key, '/') . '=/', $url)) {
|
||||
continue;
|
||||
}
|
||||
$url .= (strpos($url, '?') === false ? '?' : '&') . $pair;
|
||||
}
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Force HTML client in-browser — skip Flash / desktop-app launcher interstitial
|
||||
* that otherwise hangs on “preparing the recording/meeting”.
|
||||
*/
|
||||
public static function with_html_client(string $url): string {
|
||||
return self::append_query($url, [
|
||||
'html-view' => 'true',
|
||||
'launcher' => 'false',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recording playback URL (HTML client).
|
||||
*/
|
||||
public static function recording_play_url(string $urlpath, string $session = ''): string {
|
||||
return self::with_html_client(self::public_url($urlpath, $session));
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploaded meeting content view URL (HTML client).
|
||||
*/
|
||||
public static function content_view_url(string $urlpath, string $session = ''): string {
|
||||
return self::with_html_client(self::public_url($urlpath, $session));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adobe Connect recording editor URL (pbMode=edit).
|
||||
*/
|
||||
public static function recording_editor_url(string $urlpath, string $session = ''): string {
|
||||
$url = self::public_url($urlpath, $session);
|
||||
return self::with_html_client(self::append_query($url, [
|
||||
'pbMode' => 'edit',
|
||||
'page' => 'm',
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether settings look complete enough to call the API.
|
||||
*/
|
||||
public static function is_configured(): bool {
|
||||
return self::get_api_url() !== ''
|
||||
&& self::get_meet_host() !== ''
|
||||
&& self::get_admin_login() !== ''
|
||||
&& self::get_admin_password() !== '';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user