91334a46b5
Co-authored-by: Cursor <cursoragent@cursor.com>
398 lines
14 KiB
PHP
398 lines
14 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/>.
|
|
|
|
/**
|
|
* Audience-aware MeetLet errors (student vs teacher/admin) with help codes.
|
|
*
|
|
* @package mod_meetletacbfree
|
|
* @copyright 2026 MeetLet
|
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
|
*/
|
|
|
|
namespace mod_meetletacbfree\local;
|
|
|
|
use moodle_exception;
|
|
|
|
defined('MOODLE_INTERNAL') || die();
|
|
|
|
/**
|
|
* Structured plugin error with a public help code (ML-…) and staff-only detail.
|
|
*/
|
|
class usererror extends moodle_exception {
|
|
/** @var string Internal catalog id, e.g. sso_01 */
|
|
public string $helpid = 'err_01';
|
|
|
|
/** @var array<string, string> Staff fact rows (login, adobe_code, …) */
|
|
public array $facts = [];
|
|
|
|
/** @var string Optional Adobe XML / raw snippet */
|
|
public string $xml = '';
|
|
|
|
/**
|
|
* @param string $helpid Catalog id (sso_01, api_01, …)
|
|
* @param string $errorcode Moodle lang string id
|
|
* @param array<string, string> $facts
|
|
* @param string $xml
|
|
*/
|
|
public function __construct(string $helpid, string $errorcode, array $facts = [], string $xml = '') {
|
|
$this->helpid = $helpid !== '' ? $helpid : 'err_01';
|
|
$this->facts = $facts;
|
|
$this->xml = trim($xml);
|
|
$debug = $this->facts;
|
|
if ($this->xml !== '') {
|
|
$debug['xml'] = $this->xml;
|
|
}
|
|
parent::__construct($errorcode, 'meetletacbfree', '', null, json_encode($debug, JSON_UNESCAPED_UNICODE));
|
|
}
|
|
|
|
/**
|
|
* Teachers, managers, and site admins see technical detail; students do not.
|
|
*/
|
|
public static function is_staff(\context $context, ?int $userid = null): bool {
|
|
return has_any_capability([
|
|
'moodle/site:config',
|
|
'moodle/course:update',
|
|
'moodle/course:manageactivities',
|
|
'mod/meetletacbfree:addinstance',
|
|
'mod/meetletacbfree:host',
|
|
'mod/meetletacbfree:presenter',
|
|
], $context, $userid);
|
|
}
|
|
|
|
/**
|
|
* Help-code catalog shown in the server-setup guide.
|
|
*
|
|
* @return array<string, array{label:string,titlekey:string,fixkey:string}>
|
|
*/
|
|
public static function catalog(): array {
|
|
return [
|
|
'sso_01' => [
|
|
'label' => 'ML-SSO-01',
|
|
'titlekey' => 'helpcode_sso_01_title',
|
|
'fixkey' => 'helpcode_sso_01_fix',
|
|
],
|
|
'api_01' => [
|
|
'label' => 'ML-API-01',
|
|
'titlekey' => 'helpcode_api_01_title',
|
|
'fixkey' => 'helpcode_api_01_fix',
|
|
],
|
|
'api_02' => [
|
|
'label' => 'ML-API-02',
|
|
'titlekey' => 'helpcode_api_02_title',
|
|
'fixkey' => 'helpcode_api_02_fix',
|
|
],
|
|
'usr_01' => [
|
|
'label' => 'ML-USR-01',
|
|
'titlekey' => 'helpcode_usr_01_title',
|
|
'fixkey' => 'helpcode_usr_01_fix',
|
|
],
|
|
'mtg_01' => [
|
|
'label' => 'ML-MTG-01',
|
|
'titlekey' => 'helpcode_mtg_01_title',
|
|
'fixkey' => 'helpcode_mtg_01_fix',
|
|
],
|
|
'cfg_01' => [
|
|
'label' => 'ML-CFG-01',
|
|
'titlekey' => 'helpcode_cfg_01_title',
|
|
'fixkey' => 'helpcode_cfg_01_fix',
|
|
],
|
|
'err_01' => [
|
|
'label' => 'ML-ERR-01',
|
|
'titlekey' => 'helpcode_err_01_title',
|
|
'fixkey' => 'helpcode_err_01_fix',
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Public label for a catalog id (ML-SSO-01).
|
|
*/
|
|
public static function label(string $helpid): string {
|
|
$cat = self::catalog();
|
|
return $cat[$helpid]['label'] ?? 'ML-ERR-01';
|
|
}
|
|
|
|
/**
|
|
* Ajax payload for join / recording launch failures.
|
|
*
|
|
* @return array{code:string,message:string,detailhtml:string,staff:bool}
|
|
*/
|
|
public static function payload(\Throwable $e, \context $context): array {
|
|
if (self::is_gate_exception($e)) {
|
|
return [
|
|
'code' => '',
|
|
'message' => $e->getMessage(),
|
|
'detailhtml' => '',
|
|
'staff' => false,
|
|
];
|
|
}
|
|
|
|
$parsed = self::from_throwable($e);
|
|
$label = self::label($parsed['helpid']);
|
|
$staff = self::is_staff($context);
|
|
|
|
if (!$staff) {
|
|
return [
|
|
'code' => $label,
|
|
'message' => get_string('error_user_unavailable', 'meetletacbfree', $label),
|
|
'detailhtml' => '',
|
|
'staff' => false,
|
|
];
|
|
}
|
|
|
|
return [
|
|
'code' => $label,
|
|
'message' => self::staff_title($parsed['helpid'], $e),
|
|
'detailhtml' => self::render_staff_html($parsed),
|
|
'staff' => true,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* WS success envelope.
|
|
*
|
|
* @return array{url:string,ok:bool,helpcode:string,message:string,detailhtml:string}
|
|
*/
|
|
public static function ws_ok(string $url): array {
|
|
return [
|
|
'url' => $url,
|
|
'ok' => true,
|
|
'helpcode' => '',
|
|
'message' => '',
|
|
'detailhtml' => '',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* WS failure envelope (HTTP 200 so the UI can format HTML).
|
|
*
|
|
* @return array{url:string,ok:bool,helpcode:string,message:string,detailhtml:string}
|
|
*/
|
|
public static function ws_fail(\Throwable $e, \context $context): array {
|
|
$p = self::payload($e, $context);
|
|
return [
|
|
'url' => '',
|
|
'ok' => false,
|
|
'helpcode' => $p['code'],
|
|
'message' => $p['message'],
|
|
'detailhtml' => $p['detailhtml'],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array{url: \core_external\external_value, ok: \core_external\external_value, helpcode: \core_external\external_value, message: \core_external\external_value, detailhtml: \core_external\external_value}
|
|
*/
|
|
public static function ws_url_returns(): array {
|
|
return [
|
|
'url' => new \core_external\external_value(PARAM_RAW, 'URL with session when ok'),
|
|
'ok' => new \core_external\external_value(PARAM_BOOL, 'False when the session could not be opened'),
|
|
'helpcode' => new \core_external\external_value(PARAM_TEXT, 'Public help code, e.g. ML-SSO-01'),
|
|
'message' => new \core_external\external_value(PARAM_RAW, 'Audience-appropriate short message'),
|
|
'detailhtml' => new \core_external\external_value(PARAM_RAW, 'Staff-only HTML; empty for students'),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Pretty-print an Adobe XML snippet for a code box.
|
|
*/
|
|
public static function format_xml(string $raw): string {
|
|
$raw = trim($raw);
|
|
if ($raw === '') {
|
|
return '';
|
|
}
|
|
$start = strpos($raw, '<?xml');
|
|
if ($start === false) {
|
|
$start = strpos($raw, '<results');
|
|
}
|
|
$chunk = $start === false ? $raw : substr($raw, $start);
|
|
if (preg_match('/<results\b[\s\S]*?<\/results>/i', $chunk, $m)) {
|
|
$chunk = $m[0];
|
|
} else {
|
|
$chunk = substr($chunk, 0, 1200);
|
|
}
|
|
$dom = new \DOMDocument();
|
|
$dom->preserveWhiteSpace = false;
|
|
$dom->formatOutput = true;
|
|
if (@$dom->loadXML($chunk)) {
|
|
$out = $dom->saveXML($dom->documentElement);
|
|
return $out !== false ? trim($out) : trim($chunk);
|
|
}
|
|
return trim($chunk);
|
|
}
|
|
|
|
/**
|
|
* @return array{helpid:string,facts:array<string,string>,xml:string}
|
|
*/
|
|
public static function from_throwable(\Throwable $e): array {
|
|
if ($e instanceof self) {
|
|
return [
|
|
'helpid' => $e->helpid,
|
|
'facts' => $e->facts,
|
|
'xml' => $e->xml,
|
|
];
|
|
}
|
|
|
|
$code = $e instanceof moodle_exception ? (string)$e->errorcode : '';
|
|
$helpid = self::helpid_from_moodle($code);
|
|
$facts = [];
|
|
$xml = '';
|
|
|
|
if ($e instanceof moodle_exception && is_scalar($e->a) && (string)$e->a !== '') {
|
|
$legacy = self::parse_legacy_detail((string)$e->a);
|
|
$facts = $legacy['facts'];
|
|
$xml = $legacy['xml'];
|
|
}
|
|
|
|
return ['helpid' => $helpid, 'facts' => $facts, 'xml' => $xml];
|
|
}
|
|
|
|
protected static function is_gate_exception(\Throwable $e): bool {
|
|
if (!$e instanceof moodle_exception) {
|
|
return false;
|
|
}
|
|
return in_array($e->errorcode, [
|
|
'meetingended',
|
|
'meetingnotstarted',
|
|
'waitingforhost',
|
|
'notparticipant',
|
|
'usergrouprequired',
|
|
'joinblocked_waitinghost',
|
|
], true);
|
|
}
|
|
|
|
protected static function helpid_from_moodle(string $errorcode): string {
|
|
$map = [
|
|
'errorhttpauth' => 'sso_01',
|
|
'erroremptyxml' => 'api_01',
|
|
'errorlogin' => 'api_02',
|
|
'errorcreateuser' => 'usr_01',
|
|
'errorcreatemeeting' => 'mtg_01',
|
|
'nomeeting' => 'mtg_01',
|
|
'errorexistingsco' => 'mtg_01',
|
|
'notconfigured' => 'cfg_01',
|
|
];
|
|
return $map[$errorcode] ?? 'err_01';
|
|
}
|
|
|
|
/**
|
|
* @return array{facts:array<string,string>,xml:string}
|
|
*/
|
|
protected static function parse_legacy_detail(string $detail): array {
|
|
$facts = [];
|
|
$xml = '';
|
|
if (preg_match('/login=([^\s·]+)/u', $detail, $m)) {
|
|
$facts['login'] = $m[1];
|
|
}
|
|
if (preg_match('/\bcode=([^\s·]+)/u', $detail, $m)) {
|
|
$facts['adobe_code'] = $m[1];
|
|
}
|
|
if (preg_match('/subcode=([^\s·]+)/u', $detail, $m)) {
|
|
$facts['adobe_subcode'] = $m[1];
|
|
}
|
|
$bodypos = stripos($detail, 'body=');
|
|
if ($bodypos !== false) {
|
|
$xml = self::format_xml(substr($detail, $bodypos + 5));
|
|
}
|
|
return ['facts' => $facts, 'xml' => $xml];
|
|
}
|
|
|
|
protected static function staff_title(string $helpid, \Throwable $e): string {
|
|
$cat = self::catalog();
|
|
$key = $cat[$helpid]['titlekey'] ?? '';
|
|
if ($key !== '' && get_string_manager()->string_exists($key, 'meetletacbfree')) {
|
|
return get_string($key, 'meetletacbfree');
|
|
}
|
|
if ($e instanceof moodle_exception) {
|
|
return $e->getMessage();
|
|
}
|
|
return get_string('error', 'meetletacbfree');
|
|
}
|
|
|
|
/**
|
|
* @param array{helpid:string,facts:array<string,string>,xml:string} $parsed
|
|
*/
|
|
protected static function render_staff_html(array $parsed): string {
|
|
$helpid = $parsed['helpid'];
|
|
$label = self::label($helpid);
|
|
$cat = self::catalog();
|
|
$titlekey = $cat[$helpid]['titlekey'] ?? '';
|
|
$fixkey = $cat[$helpid]['fixkey'] ?? '';
|
|
$title = ($titlekey !== '' && get_string_manager()->string_exists($titlekey, 'meetletacbfree'))
|
|
? get_string($titlekey, 'meetletacbfree')
|
|
: get_string('error', 'meetletacbfree');
|
|
|
|
$codebox = \html_writer::div(
|
|
\html_writer::span(get_string('error_helpcode_label', 'meetletacbfree'), 'meetlet-err__codelabel') .
|
|
\html_writer::tag('code', s($label), ['data-copy-code' => $label]),
|
|
'meetlet-err__codebox'
|
|
);
|
|
|
|
$rows = '';
|
|
$factorder = ['login', 'adobe_code', 'adobe_subcode', 'egress_ip'];
|
|
$facts = $parsed['facts'];
|
|
foreach ($factorder as $key) {
|
|
if (empty($facts[$key])) {
|
|
continue;
|
|
}
|
|
$dtkey = 'errorfact_' . $key;
|
|
$dt = get_string_manager()->string_exists($dtkey, 'meetletacbfree')
|
|
? get_string($dtkey, 'meetletacbfree')
|
|
: $key;
|
|
$rows .= \html_writer::tag(
|
|
'div',
|
|
\html_writer::tag('dt', s($dt)) .
|
|
\html_writer::tag('dd', \html_writer::tag('code', s((string)$facts[$key]))),
|
|
['class' => 'meetlet-err__fact']
|
|
);
|
|
}
|
|
foreach ($facts as $key => $val) {
|
|
if (in_array($key, $factorder, true) || (string)$val === '') {
|
|
continue;
|
|
}
|
|
$rows .= \html_writer::tag(
|
|
'div',
|
|
\html_writer::tag('dt', s((string)$key)) .
|
|
\html_writer::tag('dd', \html_writer::tag('code', s((string)$val))),
|
|
['class' => 'meetlet-err__fact']
|
|
);
|
|
}
|
|
$factshtml = $rows !== '' ? \html_writer::tag('dl', $rows, ['class' => 'meetlet-err__facts']) : '';
|
|
|
|
$fix = '';
|
|
if ($fixkey !== '' && get_string_manager()->string_exists($fixkey, 'meetletacbfree')) {
|
|
$fix = \html_writer::div(get_string($fixkey, 'meetletacbfree'), 'meetlet-err__fix');
|
|
}
|
|
|
|
$xmlhtml = '';
|
|
if ($parsed['xml'] !== '') {
|
|
$xmlhtml = \html_writer::tag('p', get_string('error_adobe_response', 'meetletacbfree'), ['class' => 'meetlet-err__prelabel']) .
|
|
\html_writer::tag('pre', s($parsed['xml']), ['class' => 'meetlet-err__xml']);
|
|
}
|
|
|
|
$guide = \html_writer::tag('p', get_string('error_see_serverguide', 'meetletacbfree'), ['class' => 'meetlet-err__guide']);
|
|
|
|
return \html_writer::div(
|
|
\html_writer::tag('p', s($title), ['class' => 'meetlet-err__lead']) .
|
|
$codebox .
|
|
$factshtml .
|
|
$fix .
|
|
$xmlhtml .
|
|
$guide,
|
|
'meetlet-err meetlet-err--staff'
|
|
);
|
|
}
|
|
}
|