. /** * Adobe Connect SSO helpers for the current Moodle user. * * @package mod_meetletacbmarketplace * @copyright 2026 MeetLet * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_meetletacbmarketplace\local; use mod_meetletacbmarketplace\api\client; use mod_meetletacbmarketplace\api\service; use moodle_exception; use stdClass; defined('MOODLE_INTERNAL') || die(); /** * Build Connect logins and BREEZESESSION for Moodle users. */ class sso { /** * Ordered Connect login candidates for a Moodle user. * * @return string[] */ public static function login_candidates(stdClass $user, ?stdClass $adobeuser = null): array { $reserved = [ 'administrators', 'authors', 'everyone', 'live-admins', 'admins', 'course-admins', 'event-administrators', 'event-managers', 'learners', 'seminar-administrators', // Generic Moodle accounts must never be sent as Connect SSO identities. 'admin', 'guest', 'root', 'administrator', 'moodle', ]; $candidates = []; // Prefer ext-login (refresh_principal_login) then stable mlu{id}, then legacy logins. $seed = []; if (!empty($adobeuser->login)) { $seed[] = (string)$adobeuser->login; } if (!empty($adobeuser->internallogin)) { $seed[] = (string)$adobeuser->internallogin; } $seed[] = config::connect_login($user); foreach (array_merge($seed, config::legacy_connect_logins($user)) as $candidate) { $candidate = trim((string)$candidate); if ($candidate === '' || in_array(strtolower($candidate), $reserved, true)) { continue; } if (!in_array($candidate, $candidates, true)) { $candidates[] = $candidate; } } return $candidates; } /** * Obtain a BREEZESESSION cookie via HTTP header SSO for the given logins. * * @param string[] $logincandidates */ public static function breeze_session(array $logincandidates): string { \mod_meetletacbmarketplace\registration\manager::require_registered(); $cookie = ''; $lasterror = null; $primary = $logincandidates[0] ?? ''; foreach ($logincandidates as $logintry) { try { $sso = new client(); $cookie = $sso->login_external($logintry); if ($cookie !== '') { return $cookie; } } catch (\Throwable $e) { $lasterror = $e; } } $parsed = $lasterror ? usererror::from_throwable($lasterror) : ['helpid' => 'sso_01', 'facts' => [], 'xml' => '']; $facts = $parsed['facts']; if ($primary !== '' && empty($facts['login'])) { $facts['login'] = $primary; } $ip = self::guess_egress_ip(); if ($ip !== '') { $facts['egress_ip'] = $ip; } $helpid = $parsed['helpid']; if ($lasterror instanceof moodle_exception && $lasterror->errorcode === 'agentrequestfailed') { $helpid = 'sso_02'; $facts['agent'] = is_scalar($lasterror->a) ? (string)$lasterror->a : $lasterror->getMessage(); } if ($lasterror instanceof usererror && $lasterror->helpid === 'sso_01') { $helpid = 'sso_01'; } throw new usererror($helpid, 'errorhttpauth', $facts, $parsed['xml']); } /** * Best-effort public IP Adobe will see for Moodle API calls. */ public static function guess_egress_ip(): string { try { $curl = new \curl(['ignoresecurity' => true]); $curl->setopt(['CURLOPT_TIMEOUT' => 5]); $raw = $curl->get('https://api.ipify.org'); $ip = trim((string)$raw); if (preg_match('/^\d{1,3}(?:\.\d{1,3}){3}$/', $ip)) { return $ip; } } catch (\Throwable $e) { // Ignore. } return ''; } /** * Ensure Connect user exists, grant permission on a SCO (and parent meeting for * recordings), then SSO login. * * @return array{cookie:string,adobeuser:stdClass} */ public static function provision_and_login(stdClass $user, int $scoid, string $permission): array { \mod_meetletacbmarketplace\registration\manager::require_registered(); $service = new service(); $service->login_admin(); $adobeuser = $service->ensure_user($user); $adobeuser = $service->refresh_principal_login($adobeuser); if (trim((string)($adobeuser->login ?? '')) === '') { throw new usererror('usr_01', 'errorcreateuser', ['login' => config::connect_login($user)]); } $targets = [$scoid]; try { $info = $service->get_client()->request([ 'action' => 'sco-info', 'sco-id' => $scoid, ]); $folder = (int)($info->sco['folder-id'] ?? 0); // Parent meeting folder ACL is required for recordings and uploaded content. if ($folder > 0) { $targets[] = $folder; } } catch (\Throwable $e) { // Best-effort enrichment only. } foreach (array_unique($targets) as $aclid) { if ($aclid > 0) { $service->assign_permission((int)$adobeuser->principalid, (int)$aclid, $permission); } } $service->logout(); $cookie = self::breeze_session(self::login_candidates($user, $adobeuser)); return ['cookie' => $cookie, 'adobeuser' => $adobeuser]; } }