-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathxapi_track.php
More file actions
105 lines (93 loc) · 4.56 KB
/
Copy pathxapi_track.php
File metadata and controls
105 lines (93 loc) · 4.56 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
<?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/>.
/**
* mod_exelearning xAPI tracking endpoint (DEC-0064).
*
* The xAPI counterpart of `track.php`. The `js/xapi_listener.js` listener in `view.php`
* receives `exe-xapi-statement` postMessages from the package iframe, validates the
* origin, and POSTs each statement here. This script does the web-specific part —
* sesskey + capability authentication and the JSON response — and hands the statement
* to the shared, unit-tested {@see \mod_exelearning\local\xapi\ingestor::ingest()},
* which ignores the statement actor (grading is attributed to $USER), validates the
* statement (DEC-0063) and routes it through the existing grade pipeline (DEC-0032).
*
* Endpoint: POST with sesskey + JSON `{ id: <cmid>, statement: {...}, registration: "<token>" }`.
*
* @package mod_exelearning
* @copyright 2026 ATE (Área de Tecnología Educativa)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define('AJAX_SCRIPT', true);
require(__DIR__ . '/../../config.php');
require_once($CFG->dirroot . '/mod/exelearning/lib.php');
$cmid = required_param('id', PARAM_INT);
$mode = optional_param('mode', 'grading', PARAM_ALPHA);
require_sesskey();
$cm = get_coursemodule_from_id('exelearning', $cmid, 0, false, MUST_EXIST);
$course = $DB->get_record('course', ['id' => $cm->course], '*', MUST_EXIST);
$exelearning = $DB->get_record('exelearning', ['id' => $cm->instance], '*', MUST_EXIST);
require_login($course, false, $cm);
$context = context_module::instance($cm->id);
// Preview mode (DEC-0006): only when the caller has management capability. A regular
// student who changes ?mode=preview falls back to grading silently, like track.php.
$ispreview = ($mode === 'preview')
&& has_capability('moodle/course:manageactivities', $context);
if (!$ispreview) {
require_capability('mod/exelearning:savetrack', $context);
}
// Master switch (DEC-0064): when xAPI-primary grading is off site-wide, accept and ignore.
// The eXeLearning emitter is always-on in the export, so a statement can still reach here
// (a stale page or a crafted POST); it must not grade — the package is graded via SCORM.
if (!exelearning_xapi_primary_enabled()) {
echo json_encode(['ok' => true, 'disabled' => true]);
die;
}
$raw = file_get_contents('php://input');
$payload = $raw ? json_decode($raw, true) : null;
if (!is_array($payload) || !isset($payload['statement'])) {
throw new \moodle_exception('invalidparameter', 'error');
}
// The listener posts the statement as a nested object; tolerate a JSON-encoded string too.
$statement = is_array($payload['statement'])
? $payload['statement']
: json_decode((string) $payload['statement'], true);
if (!is_array($statement)) {
throw new \moodle_exception('invalidparameter', 'error');
}
// Bound the attempt-grouping token to the char(40) column width (and the
// PARAM_ALPHANUMEXT charset) so a long or foreign value cannot overflow
// exelearning_attempt.sessiontoken / _tracking_events.registration. A non-string body
// value is ignored; the statement's own context.registration — bounded the same way in
// the normaliser — is the fallback when the host sends none.
$registration = (isset($payload['registration']) && is_string($payload['registration']))
? substr(clean_param($payload['registration'], PARAM_ALPHANUMEXT), 0, 40) : '';
// All validation (DEC-0063), normalisation, objectid routing, idempotency, attempt
// recording, grading and completion live in the shared, unit-tested ingestor.
$result = \mod_exelearning\local\xapi\ingestor::ingest(
$exelearning,
$course,
$cm,
(int) $USER->id,
$statement,
$registration,
$ispreview
);
// The attempt cap is a conflict, not a server error: signal it with HTTP 409 as the
// listener expects (the JSON body carries the details either way).
if (!empty($result['error']) && $result['error'] === 'maxattemptsreached') {
http_response_code(409);
}
echo json_encode($result);