. /** * 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, '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, true); 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, 'lastresponse; $xmlstart = strpos($raw, '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, true); 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 ''; } }