91334a46b5
Co-authored-by: Cursor <cursoragent@cursor.com>
399 lines
13 KiB
PHP
399 lines
13 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/>.
|
|
|
|
/**
|
|
* Adobe Connect XML API client.
|
|
*
|
|
* @package mod_meetletacbfree
|
|
* @copyright 2026 MeetLet
|
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
|
*/
|
|
|
|
namespace mod_meetletacbfree\api;
|
|
|
|
use mod_meetletacbfree\local\config;
|
|
use moodle_exception;
|
|
use SimpleXMLElement;
|
|
|
|
/**
|
|
* Lightweight Adobe Connect XML API client with session reuse.
|
|
*/
|
|
class client {
|
|
/** @var string */
|
|
protected $apiurl;
|
|
/** @var int */
|
|
protected $port;
|
|
/** @var bool */
|
|
protected $https;
|
|
/** @var string */
|
|
protected $cookie = '';
|
|
/** @var string */
|
|
protected $lastresponse = '';
|
|
|
|
/**
|
|
* Constructor.
|
|
*/
|
|
public function __construct(?string $apiurl = null, ?int $port = null, ?bool $https = null) {
|
|
$this->apiurl = $apiurl ?? config::get_api_url();
|
|
$this->port = $port ?? config::get_port();
|
|
$this->https = $https ?? config::use_https();
|
|
}
|
|
|
|
/**
|
|
* Login as admin and store session cookie.
|
|
*
|
|
* @return self
|
|
*/
|
|
public function login_admin(): self {
|
|
\mod_meetletacbfree\registration\manager::require_registered();
|
|
$info = $this->request(['action' => 'common-info']);
|
|
$this->cookie = (string)($info->common->cookie ?? '');
|
|
if ($this->cookie === '') {
|
|
throw new moodle_exception('erroremptyxml', 'meetletacbfree');
|
|
}
|
|
|
|
$result = $this->request([
|
|
'action' => 'login',
|
|
'login' => config::get_admin_login(),
|
|
'password' => config::get_admin_password(),
|
|
]);
|
|
|
|
if ((string)$result->status['code'] !== 'ok') {
|
|
throw new moodle_exception('errorlogin', 'meetletacbfree');
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* External-auth (HTTP header) login for a Connect user.
|
|
*
|
|
* @param string $connectlogin Adobe Connect login (often email)
|
|
* @return string BREEZESESSION cookie value
|
|
*/
|
|
public function login_external(string $connectlogin): string {
|
|
\mod_meetletacbfree\registration\manager::require_registered();
|
|
|
|
$headername = config::get_http_auth_header();
|
|
$candidates = array_values(array_unique(array_filter([
|
|
trim($connectlogin),
|
|
])));
|
|
|
|
$lastfail = [];
|
|
foreach ($candidates as $login) {
|
|
// Preferred: XML params (same as classic adobeconnect plugin).
|
|
$cookie = $this->try_external_login_xml($headername, $login, $lastfail);
|
|
if ($cookie !== '') {
|
|
$this->cookie = $cookie;
|
|
return $cookie;
|
|
}
|
|
// Fallback: form-urlencoded (some Connect setups only accept this).
|
|
$cookie = $this->try_external_login_form($headername, $login, $lastfail);
|
|
if ($cookie !== '') {
|
|
$this->cookie = $cookie;
|
|
return $cookie;
|
|
}
|
|
}
|
|
|
|
$facts = [
|
|
'login' => (string)($lastfail['login'] ?? ($candidates[0] ?? '')),
|
|
'adobe_code' => (string)($lastfail['code'] ?? ''),
|
|
'adobe_subcode' => (string)($lastfail['subcode'] ?? 'no-login'),
|
|
];
|
|
throw new \mod_meetletacbfree\local\usererror(
|
|
'sso_01',
|
|
'errorhttpauth',
|
|
array_filter($facts),
|
|
(string)($lastfail['xml'] ?? '')
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Try SSO via XML body.
|
|
*/
|
|
protected function try_external_login_xml(string $headername, string $login, array &$lastfail): string {
|
|
$xml = $this->build_params_xml([
|
|
'action' => 'login',
|
|
'external-auth' => 'use',
|
|
]);
|
|
$raw = $this->curl_post($xml, ["{$headername}: {$login}"], true);
|
|
$this->lastresponse = $raw;
|
|
$lastfail = $this->describe_auth_failure($raw, $login);
|
|
if (!$this->response_ok($raw)) {
|
|
return '';
|
|
}
|
|
return $this->extract_breeze_cookie($raw);
|
|
}
|
|
|
|
/**
|
|
* Try SSO via application/x-www-form-urlencoded POST.
|
|
*/
|
|
protected function try_external_login_form(string $headername, string $login, array &$lastfail): string {
|
|
$raw = $this->curl_post_form(
|
|
'action=login&external-auth=use',
|
|
["{$headername}: {$login}"],
|
|
true
|
|
);
|
|
$this->lastresponse = $raw;
|
|
$lastfail = $this->describe_auth_failure($raw, $login);
|
|
if (!$this->response_ok($raw)) {
|
|
return '';
|
|
}
|
|
return $this->extract_breeze_cookie($raw);
|
|
}
|
|
|
|
/**
|
|
* Structured Adobe SSO failure (login, code, subcode, xml).
|
|
*
|
|
* @return array{login:string,code:string,subcode:string,xml:string}
|
|
*/
|
|
protected function describe_auth_failure(string $raw, string $login): array {
|
|
$xmlstart = strpos($raw, '<?xml');
|
|
if ($xmlstart === false) {
|
|
$xmlstart = strpos($raw, '<results');
|
|
}
|
|
$code = '';
|
|
$sub = '';
|
|
$sx = @simplexml_load_string(($xmlstart === false) ? $raw : substr($raw, $xmlstart));
|
|
if ($sx !== false) {
|
|
$code = (string)($sx->status['code'] ?? '');
|
|
$sub = (string)($sx->status['subcode'] ?? '');
|
|
}
|
|
return [
|
|
'login' => $login,
|
|
'code' => $code,
|
|
'subcode' => $sub,
|
|
'xml' => \mod_meetletacbfree\local\usererror::format_xml($raw),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* POST form fields to Adobe Connect.
|
|
*
|
|
* @param string $body
|
|
* @param array $extraheaders
|
|
* @param bool $includeheader
|
|
* @return string
|
|
*/
|
|
protected function curl_post_form(string $body, array $extraheaders = [], bool $includeheader = false): string {
|
|
global $CFG;
|
|
|
|
$url = $this->apiurl;
|
|
if ($this->https && stripos($url, 'http://') === 0) {
|
|
$url = 'https://' . substr($url, 7);
|
|
} else if ($this->https && stripos($url, 'https://') !== 0 && stripos($url, 'http://') !== 0) {
|
|
$url = 'https://' . $url;
|
|
}
|
|
|
|
$ch = curl_init();
|
|
curl_setopt($ch, CURLOPT_URL, $url);
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
curl_setopt($ch, CURLOPT_POST, true);
|
|
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
|
|
curl_setopt($ch, CURLOPT_PORT, $this->port);
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, array_merge(
|
|
['Content-Type: application/x-www-form-urlencoded'],
|
|
$extraheaders
|
|
));
|
|
curl_setopt($ch, CURLOPT_HEADER, $includeheader ? 1 : 0);
|
|
if (!empty($_SERVER['HTTP_USER_AGENT'])) {
|
|
curl_setopt($ch, CURLOPT_USERAGENT, $_SERVER['HTTP_USER_AGENT']);
|
|
}
|
|
if ($this->https) {
|
|
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
|
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
|
|
}
|
|
if (!empty($CFG->proxyhost)) {
|
|
curl_setopt($ch, CURLOPT_PROXY, $CFG->proxyhost);
|
|
if (!empty($CFG->proxyport)) {
|
|
curl_setopt($ch, CURLOPT_PROXYPORT, $CFG->proxyport);
|
|
}
|
|
}
|
|
|
|
$result = curl_exec($ch);
|
|
if ($result === false) {
|
|
$err = curl_error($ch);
|
|
curl_close($ch);
|
|
throw new moodle_exception('erroremptyxml', 'meetletacbfree', '', null, $err);
|
|
}
|
|
curl_close($ch);
|
|
return (string)$result;
|
|
}
|
|
|
|
/**
|
|
* Logout current session.
|
|
*/
|
|
public function logout(): void {
|
|
if ($this->cookie === '') {
|
|
return;
|
|
}
|
|
try {
|
|
$this->request(['action' => 'logout']);
|
|
} catch (\Throwable $e) {
|
|
// Ignore logout failures.
|
|
}
|
|
$this->cookie = '';
|
|
}
|
|
|
|
/**
|
|
* Perform an API action and return SimpleXMLElement.
|
|
*
|
|
* @param array $params
|
|
* @return SimpleXMLElement
|
|
*/
|
|
public function request(array $params): SimpleXMLElement {
|
|
$xml = $this->build_params_xml($params);
|
|
$raw = $this->curl_post($xml);
|
|
$this->lastresponse = $raw;
|
|
|
|
$xmlstart = strpos($raw, '<?xml');
|
|
if ($xmlstart === false) {
|
|
$xmlstart = strpos($raw, '<results');
|
|
}
|
|
$body = ($xmlstart === false) ? $raw : substr($raw, $xmlstart);
|
|
|
|
if ($body === '' || $body === false) {
|
|
throw new moodle_exception('erroremptyxml', 'meetletacbfree');
|
|
}
|
|
|
|
$sx = @simplexml_load_string($body);
|
|
if ($sx === false) {
|
|
throw new moodle_exception('erroremptyxml', 'meetletacbfree');
|
|
}
|
|
|
|
return $sx;
|
|
}
|
|
|
|
/**
|
|
* Whether last/raw response status is ok.
|
|
*
|
|
* @param string|null $raw
|
|
* @return bool
|
|
*/
|
|
public function response_ok(?string $raw = null): bool {
|
|
$raw = $raw ?? $this->lastresponse;
|
|
$xmlstart = strpos($raw, '<?xml');
|
|
if ($xmlstart === false) {
|
|
$xmlstart = strpos($raw, '<results');
|
|
}
|
|
$body = ($xmlstart === false) ? $raw : substr($raw, $xmlstart);
|
|
$sx = @simplexml_load_string($body);
|
|
if ($sx === false) {
|
|
return false;
|
|
}
|
|
return (string)$sx->status['code'] === 'ok';
|
|
}
|
|
|
|
/**
|
|
* Get session cookie.
|
|
*/
|
|
public function get_cookie(): string {
|
|
return $this->cookie;
|
|
}
|
|
|
|
/**
|
|
* Get raw last response.
|
|
*/
|
|
public function get_last_response(): string {
|
|
return $this->lastresponse;
|
|
}
|
|
|
|
/**
|
|
* Build XML params payload.
|
|
*
|
|
* @param array $params
|
|
* @return string
|
|
*/
|
|
protected function build_params_xml(array $params): string {
|
|
$dom = new \DOMDocument('1.0', 'UTF-8');
|
|
$root = $dom->createElement('params');
|
|
$dom->appendChild($root);
|
|
foreach ($params as $name => $value) {
|
|
$child = $dom->createElement('param', htmlspecialchars((string)$value, ENT_XML1 | ENT_COMPAT, 'UTF-8'));
|
|
$child->setAttribute('name', (string)$name);
|
|
$root->appendChild($child);
|
|
}
|
|
return $dom->saveXML();
|
|
}
|
|
|
|
/**
|
|
* POST XML to Adobe Connect.
|
|
*
|
|
* @param string $xml
|
|
* @param array $extraheaders
|
|
* @param bool $includeheader
|
|
* @return string
|
|
*/
|
|
protected function curl_post(string $xml, array $extraheaders = [], bool $includeheader = false): string {
|
|
global $CFG;
|
|
|
|
$url = $this->apiurl;
|
|
if ($this->https && stripos($url, 'http://') === 0) {
|
|
$url = 'https://' . substr($url, 7);
|
|
} else if ($this->https && stripos($url, 'https://') !== 0 && stripos($url, 'http://') !== 0) {
|
|
$url = 'https://' . $url;
|
|
}
|
|
|
|
if ($this->cookie !== '') {
|
|
$url .= (strpos($url, '?') === false ? '?' : '&') . 'session=' . rawurlencode($this->cookie);
|
|
}
|
|
|
|
$ch = curl_init();
|
|
curl_setopt($ch, CURLOPT_URL, $url);
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
curl_setopt($ch, CURLOPT_POST, true);
|
|
curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);
|
|
curl_setopt($ch, CURLOPT_PORT, $this->port);
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, array_merge(['Content-Type: text/xml'], $extraheaders));
|
|
curl_setopt($ch, CURLOPT_HEADER, $includeheader ? 1 : 0);
|
|
if (!empty($_SERVER['HTTP_USER_AGENT'])) {
|
|
curl_setopt($ch, CURLOPT_USERAGENT, $_SERVER['HTTP_USER_AGENT']);
|
|
}
|
|
if ($this->https) {
|
|
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
|
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
|
|
}
|
|
if (!empty($CFG->proxyhost)) {
|
|
curl_setopt($ch, CURLOPT_PROXY, $CFG->proxyhost);
|
|
if (!empty($CFG->proxyport)) {
|
|
curl_setopt($ch, CURLOPT_PROXYPORT, $CFG->proxyport);
|
|
}
|
|
}
|
|
|
|
$result = curl_exec($ch);
|
|
if ($result === false) {
|
|
$err = curl_error($ch);
|
|
curl_close($ch);
|
|
throw new moodle_exception('erroremptyxml', 'meetletacbfree', '', null, $err);
|
|
}
|
|
curl_close($ch);
|
|
return (string)$result;
|
|
}
|
|
|
|
/**
|
|
* Extract BREEZESESSION from header+body response.
|
|
*
|
|
* @param string $raw
|
|
* @return string
|
|
*/
|
|
protected function extract_breeze_cookie(string $raw): string {
|
|
if (preg_match('/BREEZESESSION=([^;\s]+)/', $raw, $m)) {
|
|
return $m[1];
|
|
}
|
|
return '';
|
|
}
|
|
}
|