91334a46b5
Co-authored-by: Cursor <cursoragent@cursor.com>
197 lines
7.1 KiB
PHP
197 lines
7.1 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/>.
|
|
|
|
/**
|
|
* Meeting content download helpers (Adobe Connect proxy).
|
|
*
|
|
* @package mod_meetletacbfree
|
|
* @copyright 2026 MeetLet <info@meetlet.net>
|
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
|
*/
|
|
|
|
namespace mod_meetletacbfree\local;
|
|
|
|
use mod_meetletacbfree\api\service;
|
|
use stdClass;
|
|
|
|
defined('MOODLE_INTERNAL') || die();
|
|
|
|
/**
|
|
* Authenticated Moodle proxy for uploaded meeting content files.
|
|
*/
|
|
class content {
|
|
/**
|
|
* Opaque, HMAC-signed, current-user download URL.
|
|
*/
|
|
public static function download_url(int $userid, int $cmid, int $scoid): \moodle_url {
|
|
return new \moodle_url('/mod/meetletacbfree/download_content.php', [
|
|
'k' => self::make_download_token($userid, $cmid, $scoid),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Unpack and verify a content download token.
|
|
*
|
|
* @return array{userid:int,cmid:int,cid:int}
|
|
*/
|
|
public static function unpack_download_token(string $token): array {
|
|
$token = trim($token);
|
|
if (!preg_match('/^[A-Za-z0-9_-]+$/', $token)) {
|
|
throw new \moodle_exception('errorbadcontenttoken', 'meetletacbfree');
|
|
}
|
|
$b64 = strtr($token, '-_', '+/');
|
|
$pad = strlen($b64) % 4;
|
|
if ($pad) {
|
|
$b64 .= str_repeat('=', 4 - $pad);
|
|
}
|
|
$raw = base64_decode($b64, true);
|
|
if ($raw === false || strlen($raw) < 44) {
|
|
throw new \moodle_exception('errorbadcontenttoken', 'meetletacbfree');
|
|
}
|
|
$mac = substr($raw, 0, 32);
|
|
$payload = substr($raw, 32);
|
|
if (strlen($payload) < 32) {
|
|
throw new \moodle_exception('errorbadcontenttoken', 'meetletacbfree');
|
|
}
|
|
|
|
$ids = unpack('Nuid/Ncmid/Ncid/Nexp', substr($payload, 0, 16));
|
|
$nonce = substr($payload, 16, 16);
|
|
$userid = (int)$ids['uid'];
|
|
$cmid = (int)$ids['cmid'];
|
|
$cid = (int)$ids['cid'];
|
|
$expires = (int)$ids['exp'];
|
|
$expected = hash_hmac(
|
|
'sha256',
|
|
'meetletacbfree-content-v1|' . $userid . ':' . $cmid . ':' . $cid . ':' . $expires . '|' . $nonce,
|
|
self::download_secret(),
|
|
true
|
|
);
|
|
if (!hash_equals($expected, $mac)) {
|
|
throw new \moodle_exception('errorbadcontenttoken', 'meetletacbfree');
|
|
}
|
|
if ($expires < time() || $userid <= 0 || $cmid <= 0 || $cid <= 0) {
|
|
throw new \moodle_exception('errorbadcontenttoken', 'meetletacbfree');
|
|
}
|
|
return ['userid' => $userid, 'cmid' => $cmid, 'cid' => $cid];
|
|
}
|
|
|
|
/**
|
|
* Find a content item listed under a meeting folder.
|
|
*/
|
|
public static function find_in_meeting(service $service, int $meetingscoid, int $scoid): ?stdClass {
|
|
foreach ($service->list_meeting_content($meetingscoid) as $item) {
|
|
if ((int)($item->scoid ?? 0) === $scoid) {
|
|
return $item;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Adobe stores uploaded binaries under {url-path}/output/{filename}.
|
|
*/
|
|
public static function output_file_url(string $urlpath, string $filename): string {
|
|
$filename = basename(str_replace('\\', '/', $filename));
|
|
if ($filename === '' || strpos($filename, '/') !== false) {
|
|
throw new \moodle_exception('filenotfound', 'error');
|
|
}
|
|
$base = '/' . trim($urlpath, '/');
|
|
if ($base === '/') {
|
|
throw new \moodle_exception('filenotfound', 'error');
|
|
}
|
|
return config::public_url($base . '/output/' . rawurlencode($filename));
|
|
}
|
|
|
|
/**
|
|
* Stream a meeting content file to the browser via user SSO.
|
|
*/
|
|
public static function serve_download(instance $inst, int $scoid): void {
|
|
global $USER;
|
|
|
|
$service = new service();
|
|
$service->login_admin();
|
|
$item = self::find_in_meeting($service, $inst->require_meeting(), $scoid);
|
|
$service->logout();
|
|
|
|
if (!$item || empty($item->name) || empty($item->urlpath)) {
|
|
throw new \moodle_exception('filenotfound', 'error');
|
|
}
|
|
|
|
$adoberole = roles::resolve_adobe_role($inst->record, $inst->context, (int)$USER->id);
|
|
$permission = $adoberole ?: service::ROLE_PARTICIPANT;
|
|
$auth = sso::provision_and_login($USER, $scoid, $permission);
|
|
|
|
$fileurl = self::output_file_url((string)$item->urlpath, (string)$item->name);
|
|
$filename = clean_filename((string)$item->name);
|
|
if ($filename === '') {
|
|
$filename = 'content_' . $scoid;
|
|
}
|
|
|
|
$tmpdir = make_request_directory();
|
|
$tmp = $tmpdir . '/content_' . $scoid . '_' . $filename;
|
|
self::fetch_to_path($fileurl, $auth['cookie'], $tmp);
|
|
|
|
\core\session\manager::write_close();
|
|
send_temp_file($tmp, $filename);
|
|
}
|
|
|
|
/**
|
|
* Download remote content bytes using a BREEZESESSION cookie.
|
|
*/
|
|
protected static function fetch_to_path(string $url, string $session, string $dest): void {
|
|
$ch = curl_init($url);
|
|
$fp = fopen($dest, 'wb');
|
|
if ($fp === false) {
|
|
curl_close($ch);
|
|
throw new \moodle_exception('filenotfound', 'error');
|
|
}
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_FILE => $fp,
|
|
CURLOPT_FOLLOWLOCATION => true,
|
|
CURLOPT_SSL_VERIFYPEER => false,
|
|
CURLOPT_TIMEOUT => 300,
|
|
CURLOPT_COOKIE => $session !== '' ? ('BREEZESESSION=' . $session) : '',
|
|
]);
|
|
$ok = curl_exec($ch);
|
|
$code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
$size = (int)curl_getinfo($ch, CURLINFO_SIZE_DOWNLOAD);
|
|
curl_close($ch);
|
|
fclose($fp);
|
|
|
|
if (!$ok || $code < 200 || $code >= 400 || $size <= 0 || !is_file($dest) || filesize($dest) <= 0) {
|
|
@unlink($dest);
|
|
throw new \moodle_exception('filenotfound', 'error');
|
|
}
|
|
}
|
|
|
|
protected static function make_download_token(int $userid, int $cmid, int $scoid): string {
|
|
$expires = time() + DAYSECS * 7;
|
|
$nonce = random_bytes(16);
|
|
$payload = pack('N4', $userid, $cmid, $scoid, $expires) . $nonce;
|
|
$mac = hash_hmac(
|
|
'sha256',
|
|
'meetletacbfree-content-v1|' . $userid . ':' . $cmid . ':' . $scoid . ':' . $expires . '|' . $nonce,
|
|
self::download_secret(),
|
|
true
|
|
);
|
|
return rtrim(strtr(base64_encode($mac . $payload), '+/', '-_'), '=');
|
|
}
|
|
|
|
protected static function download_secret(): string {
|
|
return 'mod_meetletacbfree/content/' . get_site_identifier();
|
|
}
|
|
}
|