Commit 1d4c6f53 authored by Ferran Recio Calderó's avatar Ferran Recio Calderó
Browse files

MDL-67734 core_xapi: add xAPI statement support webservice

parent 0a832fa1
<?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/>.
/**
* Strings for xapi library, language 'en'
*
* @package core_xapi
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['eventxapipost'] = 'Post xAPI statement';
$string['privacy:metadata'] = 'The xAPI library does not store any personal data.';
......@@ -108,6 +108,7 @@
"timezones": null,
"user": "user",
"userkey": "lib\/userkey",
"webservice": "webservice"
"webservice": "webservice",
"xapi": "lib\/xapi"
}
}
......@@ -2744,6 +2744,16 @@ $functions = array(
'capabilities' => '',
'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE],
],
'core_xapi_statement_post' => [
'classname' => 'core_xapi\external\post_statement',
'methodname' => 'execute',
'classpath' => '',
'description' => 'Post an xAPI statement.',
'type' => 'write',
'ajax' => 'true',
'capabilities' => '',
'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE],
],
);
$services = array(
......
......@@ -36,7 +36,7 @@ class core_component_testcase extends advanced_testcase {
* this is defined here to annoy devs that try to add more without any thinking,
* always verify that it does not collide with any existing add-on modules and subplugins!!!
*/
const SUBSYSTEMCOUNT = 69;
const SUBSYSTEMCOUNT = 70;
public function setUp() {
$psr0namespaces = new ReflectionProperty('core_component', 'psr0namespaces');
......
<?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/>.
/**
* This is the external API for generic xAPI handling.
*
* @package core_xapi
* @since Moodle 3.9
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_xapi\external;
use core_xapi\local\statement;
use core_xapi\handler;
use core_xapi\xapi_exception;
use external_api;
use external_function_parameters;
use external_value;
use external_single_structure;
use external_multiple_structure;
use external_warnings;
use core_component;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir .'/externallib.php');
/**
* This is the external API for generic xAPI handling.
*
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class post_statement extends external_api {
/**
* Parameters for execute
*
* @return external_function_parameters
*/
public static function execute_parameters() {
return new external_function_parameters(
[
'component' => new external_value(PARAM_COMPONENT, 'Component name', VALUE_REQUIRED),
'requestjson' => new external_value(PARAM_RAW, 'json object with all the statements to post', VALUE_REQUIRED)
]
);
}
/**
* Process a statement post request.
*
* @param string $component component name (frankenstyle)
* @param string $requestjson json object with all the statements to post
* @return bool[] storing acceptance of every statement
*/
public static function execute(string $component, string $requestjson): array {
$params = self::validate_parameters(self::execute_parameters(), array(
'component' => $component,
'requestjson' => $requestjson,
));
$component = $params['component'];
$requestjson = $params['requestjson'];
static::validate_component($component);
$handler = handler::create($component);
$statements = self::get_statements_from_json($requestjson);
if (!self::check_statements_users($statements, $handler)) {
throw new xapi_exception('Statements actor is not the current user');
}
$result = $handler->process_statements($statements);
// In case no statement is processed, an error must be returned.
if (count(array_filter($result)) == 0) {
throw new xapi_exception('No statement can be processed.');
}
return $result;
}
/**
* Return for execute.
*/
public static function execute_returns() {
return new external_multiple_structure(
new external_value(PARAM_BOOL, 'If the statement is accepted'),
'List of statements storing acceptance results'
);
}
/**
* Check component name.
*
* Note: this function is separated mainly for testing purposes to
* be overridden to fake components.
*
* @throws xapi_exception if component is not available
* @param string $component component name
*/
protected static function validate_component(string $component): void {
// Check that $component is a real component name.
$dir = core_component::get_component_directory($component);
if (!$dir) {
throw new xapi_exception("Component $component not available.");
}
}
/**
* Convert mulitple types of statement request into an array of statements.
*
* @throws xapi_exception if JSON cannot be parsed
* @param string $requestjson json encoded statements structure
* @return statement[] array of statements
*/
private static function get_statements_from_json(string $requestjson): array {
$request = json_decode($requestjson);
if ($request === null) {
throw new xapi_exception('JSON error: '.json_last_error_msg());
}
$result = [];
if (is_array($request)) {
foreach ($request as $data) {
$result[] = statement::create_from_data($data);
}
} else {
$result[] = statement::create_from_data($request);
}
if (empty($result)) {
throw new xapi_exception('No statements detected');
}
return $result;
}
/**
* Check that $USER is actor in all statements.
*
* @param statement[] $statements array of statements
* @param handler $handler specific xAPI handler
* @return bool if $USER is actor in all statements
*/
private static function check_statements_users(array $statements, handler $handler): bool {
global $USER;
foreach ($statements as $statement) {
if ($handler->supports_group_actors()) {
$users = $statement->get_all_users();
if (!isset($users[$USER->id])) {
return false;
}
} else {
$user = $statement->get_user();
if ($user->id != $USER->id) {
return false;
}
}
}
return true;
}
}
<?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/>.
/**
* The core_xapi statement validation and tansformation.
*
* @package core_xapi
* @since Moodle 3.9
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_xapi;
use core_xapi\local\statement;
use core_xapi\xapi_exception;
use stdClass;
defined('MOODLE_INTERNAL') || die();
/**
* Class handler handles basic xapi statements.
*
* @package core_xapi
* @copyright 2020 Ferran Recio
*/
abstract class handler {
/** @var string component name in frankenstyle. */
protected $component;
/**
* Constructor for a xAPI handler base class.
*
* @param string $component the component name
*/
final protected function __construct(string $component) {
$this->component = $component;
}
/**
* Returns the xAPI handler of a specific component.
*
* @param string $component the component name in frankenstyle.
* @return handler|null a handler object or null if none found.
* @throws xapi_exception
*/
final public static function create(string $component): self {
$classname = "\\$component\\xapi\\handler";
if (class_exists($classname)) {
return new $classname($component);
}
throw new xapi_exception('Unknown handler');
}
/**
* Convert a statement object into a Moodle xAPI Event.
*
* If a statement is accepted by validate_statement the component must provide a event
* to handle that statement, otherwise the statement will be rejected.
*
* Note: this method must be overridden by the plugins which want to use xAPI.
*
* @param statement $statement
* @return \core\event\base|null a Moodle event to trigger
*/
abstract public function statement_to_event(statement $statement): ?\core\event\base;
/**
* Return true if group actor is enabled.
*
* Note: this method must be overridden by the plugins which want to
* use groups in statements.
*
* @return bool
*/
public function supports_group_actors(): bool {
return false;
}
/**
* Process a bunch of statements sended to a specific component.
*
* @param statement[] $statements an array with all statement to process.
* @return int[] return an specifying what statements are being stored.
*/
public function process_statements(array $statements): array {
$result = [];
foreach ($statements as $key => $statement) {
try {
// Ask the plugin to convert into an event.
$event = $this->statement_to_event($statement);
if ($event) {
$event->trigger();
$result[$key] = true;
} else {
$result[$key] = false;
}
} catch (\Exception $e) {
$result[$key] = false;
}
}
return $result;
}
}
<?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/>.
/**
* xAPI LRS IRI values generator.
*
* @package core_xapi
* @since Moodle 3.9
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_xapi;
defined('MOODLE_INTERNAL') || die();
use stdClass;
use moodle_url;
/**
* Class to translate Moodle objects to xAPI elements.
*
* @copyright 2020 Ferran Recio
* @since Moodle 3.9
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class iri {
/**
* Generate a valid IRI element from a $value and an optional $type.
*
* Verbs and Objects in xAPI are in IRI format. This function could get
* a valid IRI value (and will return without modifiyng it) or a simple
* string and a type and generate a fake IRI valir for any xAPI statement.
*
* @param string $value a valid IRI value or any string
* @param string|null $type if none passed $type will be 'element'
* @return string a valid IRI value
*/
public static function generate(string $value, string $type = null): string {
if (self::check($value)) {
return $value;
}
if (empty($type)) {
$type = 'element';
}
return (new moodle_url("/xapi/$type/$value"))->out(false);
}
/**
* Try to extract the original value from an IRI.
*
* If a real IRI value is passed, it will return it without any change. If a
* fake IRI is passed (generated by iri::generate)
* it will try to extract the original value.
*
* @param string $value the currewnt IRI value.
* @param string|null $type if $value is a fake IRI, the $type must be provided.
* @return string the original value used in iri::generate.
*/
public static function extract(string $value, string $type = null): string {
if (empty($type)) {
$type = 'element';
}
$xapibase = (new moodle_url("/xapi/$type/"))->out(false);
if (strpos($value, $xapibase) === 0) {
return substr($value, strlen($xapibase));
}
return $value;
}
/**
* Check if a $value could be a valid IRI or not.
*
* @param string $value the currewnt IRI value.
* @return bool if the $value could be an IRI.
*/
public static function check(string $value): bool {
$iri = new moodle_url($value);
return in_array($iri->get_scheme(), ['http', 'https']);
}
}
<?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/>.
/**
* Statement base object for xAPI structure checking and validation.
*
* @package core_xapi
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_xapi\local;
use core_xapi\local\statement\item;
use core_xapi\local\statement\item_actor;
use core_xapi\local\statement\item_object;
use core_xapi\local\statement\item_verb;
use core_xapi\xapi_exception;
use JsonSerializable;
use stdClass;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for core_xapi implementing null_provider.
*
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class statement implements JsonSerializable {
/** @var actor The statement actor. */
protected $actor = null;
/** @var verb The statement verb. */
protected $verb = null;
/** @var object The statement object. */
protected $object = null;
/** @var result The statement result. */
protected $result = null;
/** @var context The statement context. */
protected $context = null;
/** @var timestamp The statement timestamp. */
protected $timestamp = null;
/** @var stored The statement stored. */
protected $stored = null;
/** @var authority The statement authority. */
protected $authority = null;
/** @var version The statement version. */
protected $version = null;
/** @var attachments The statement attachments. */
protected $attachments = null;
/** @var additionalfields list of additional fields. */
private static $additionalsfields = [
'context', 'result', 'timestamp', 'stored', 'authority', 'version', 'attachments'
];
/**
* Function to create a full statement from xAPI statement data.
*
* @param stdClass $data the original xAPI statement
* @return statement statement object
*/
public static function create_from_data(stdClass $data): self {
$result = new self();
$requiredfields = ['actor', 'verb', 'object'];
foreach ($requiredfields as $required) {
if (!isset($data->$required)) {
throw new xapi_exception("Missing '{$required}'");
}
}
$result->set_actor(item_actor::create_from_data($data->actor));
$result->set_verb(item_verb::create_from_data($data->verb));
$result->set_object(item_object::create_from_data($data->object));
// Store other generic xAPI statement fields.
foreach (self::$additionalsfields as $additional) {
if (isset($data->$additional)) {
$method = 'set_'.$additional;
$result->$method(item::create_from_data($data->$additional));
}
}
return $result;
}
/**
* Return the data to serialize in case JSON statement is needed.
*
* @return stdClass the statement data structure
*/
public function jsonSerialize(): stdClass {
$result = (object) [
'actor' => $this->actor,
'verb' => $this->verb,
'object' => $this->object,
];
foreach (self::$additionalsfields as $additional) {
if (!empty($this->$additional)) {
$result->$additional = $this->$additional;
}
}
return $result;
}
/**
* Returns a minified version of a given statement.
*
* The returned structure is suitable to store in the "other" field
* of logstore. xAPI standard specifies a list of attributes that can be calculated
* instead of stored literally. This function get rid of these attributes.
*
* Note: it also converts stdClass to assoc array to make it compatible
* with "other" field in the logstore
*
* @return array the minimal statement needed to be stored a part from logstore data
*/
public function minify(): ?array {
$result = [];
$fields = ['verb', 'object', 'context', 'result', 'authority', 'attachments'];
foreach ($fields as $field) {
if (!empty($this->$field)) {
$result[$field] = $this->$field;
}
}
return json_decode(json_encode($result), true);
}
/**
* Set the statement actor.
*
* @param item_actor $actor actor item
*/
public function set_actor(item_actor $actor): void {
$this->actor = $actor;
}
/**
* Set the statement verb.
*
* @param item_verb $verb verb element
*/
public function set_verb(item_verb $verb): void {
$this->verb = $verb;