Files
meetletacbfree/classes/local/usererror.php
T

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'
);
}
}