This commit is contained in:
Xes
2025-08-14 22:39:38 +02:00
parent 3641e93527
commit 5403f346e3
3370 changed files with 327179 additions and 0 deletions

1
plugin/xapi/.htaccess Normal file
View File

@@ -0,0 +1 @@
AcceptPathInfo On

88
plugin/xapi/README.md Normal file
View File

@@ -0,0 +1,88 @@
# Experience API (xAPI)
Allows you to connect to an external Learning Record Store and use activities with the xAPI standard.
> You can import and use TinCan packages.
> Import CMI5 packages is to be considered a Beta state and still in development.
**Configuration**
Set LRS endpoint, username and password to integrate an external LRS in Chamilo LMS.
The fields "Learning path item viewed", "Learning path ended", "Quiz question answered" and "Quiz ended" allow enabling
hooks when the user views an item in learning path, completes a learning path, answers a quiz question and ends the exam.
The statements generated with these hooks are logged in Chamilo database, waiting to be sent to the LRS by a cron job.
The cron job to configure on your server is located in `CHAMILO_PATH/plugin/xapi/cron/send_statements.php`.
**Use the Statement API from Chamilo LMS**
You can use xAPI's "Statement API" to save some statements from another service.
You need to create credentials (username/password) to do this. First you need to enable the "menu_administrator" region
in the plugin configuration. You will then be able to create the credentials with the new page "Experience API (xAPI)"
inside de Plugins block in the Administration panel.
The endpoint for the statements API is "https://CHAMILO_DOMAIN/plugin/xapi/lrs.php/";
```mysql
CREATE TABLE xapi_attachment (identifier INT AUTO_INCREMENT NOT NULL, statement_id VARCHAR(255) DEFAULT NULL, usageType VARCHAR(255) NOT NULL, contentType VARCHAR(255) NOT NULL, length INT NOT NULL, sha2 VARCHAR(255) NOT NULL, display LONGTEXT NOT NULL COMMENT '(DC2Type:json)', hasDescription TINYINT(1) NOT NULL, description LONGTEXT DEFAULT NULL COMMENT '(DC2Type:json)', fileUrl VARCHAR(255) DEFAULT NULL, content LONGTEXT DEFAULT NULL, INDEX IDX_7148C9A1849CB65B (statement_id), PRIMARY KEY(identifier)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB;
CREATE TABLE xapi_object (identifier INT AUTO_INCREMENT NOT NULL, group_id INT DEFAULT NULL, actor_id INT DEFAULT NULL, verb_id INT DEFAULT NULL, object_id INT DEFAULT NULL, type VARCHAR(255) DEFAULT NULL, activityId VARCHAR(255) DEFAULT NULL, hasActivityDefinition TINYINT(1) DEFAULT NULL, hasActivityName TINYINT(1) DEFAULT NULL, activityName LONGTEXT DEFAULT NULL COMMENT '(DC2Type:json)', hasActivityDescription TINYINT(1) DEFAULT NULL, activityDescription LONGTEXT DEFAULT NULL COMMENT '(DC2Type:json)', activityType VARCHAR(255) DEFAULT NULL, activityMoreInfo VARCHAR(255) DEFAULT NULL, mbox VARCHAR(255) DEFAULT NULL, mboxSha1Sum VARCHAR(255) DEFAULT NULL, openId VARCHAR(255) DEFAULT NULL, accountName VARCHAR(255) DEFAULT NULL, accountHomePage VARCHAR(255) DEFAULT NULL, name VARCHAR(255) DEFAULT NULL, referenced_statement_id VARCHAR(255) DEFAULT NULL, activityExtensions_id INT DEFAULT NULL, parentContext_id INT DEFAULT NULL, groupingContext_id INT DEFAULT NULL, categoryContext_id INT DEFAULT NULL, otherContext_id INT DEFAULT NULL, UNIQUE INDEX UNIQ_E2B68640303C7F1D (activityExtensions_id), INDEX IDX_E2B68640FE54D947 (group_id), UNIQUE INDEX UNIQ_E2B6864010DAF24A (actor_id), UNIQUE INDEX UNIQ_E2B68640C1D03483 (verb_id), UNIQUE INDEX UNIQ_E2B68640232D562B (object_id), INDEX IDX_E2B68640988A4CEC (parentContext_id), INDEX IDX_E2B686404F542860 (groupingContext_id), INDEX IDX_E2B68640AEA1B132 (categoryContext_id), INDEX IDX_E2B68640B73EEAB7 (otherContext_id), PRIMARY KEY(identifier)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB;
CREATE TABLE xapi_result (identifier INT AUTO_INCREMENT NOT NULL, extensions_id INT DEFAULT NULL, hasScore TINYINT(1) NOT NULL, scaled DOUBLE PRECISION DEFAULT NULL, raw DOUBLE PRECISION DEFAULT NULL, min DOUBLE PRECISION DEFAULT NULL, max DOUBLE PRECISION DEFAULT NULL, success TINYINT(1) DEFAULT NULL, completion TINYINT(1) DEFAULT NULL, response VARCHAR(255) DEFAULT NULL, duration VARCHAR(255) DEFAULT NULL, UNIQUE INDEX UNIQ_5971ECBFD0A19400 (extensions_id), PRIMARY KEY(identifier)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB;
CREATE TABLE xapi_verb (identifier INT AUTO_INCREMENT NOT NULL, id VARCHAR(255) NOT NULL, display LONGTEXT NOT NULL COMMENT '(DC2Type:json)', PRIMARY KEY(identifier)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB;
CREATE TABLE xapi_extensions (identifier INT AUTO_INCREMENT NOT NULL, extensions LONGTEXT NOT NULL COMMENT '(DC2Type:json)', PRIMARY KEY(identifier)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB;
CREATE TABLE xapi_context (identifier INT AUTO_INCREMENT NOT NULL, instructor_id INT DEFAULT NULL, team_id INT DEFAULT NULL, extensions_id INT DEFAULT NULL, registration VARCHAR(255) DEFAULT NULL, hasContextActivities TINYINT(1) DEFAULT NULL, revision VARCHAR(255) DEFAULT NULL, platform VARCHAR(255) DEFAULT NULL, language VARCHAR(255) DEFAULT NULL, statement VARCHAR(255) DEFAULT NULL, UNIQUE INDEX UNIQ_3D7771908C4FC193 (instructor_id), UNIQUE INDEX UNIQ_3D777190296CD8AE (team_id), UNIQUE INDEX UNIQ_3D777190D0A19400 (extensions_id), PRIMARY KEY(identifier)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB;
CREATE TABLE xapi_actor (identifier INT AUTO_INCREMENT NOT NULL, type VARCHAR(255) DEFAULT NULL, mbox VARCHAR(255) DEFAULT NULL, mboxSha1Sum VARCHAR(255) DEFAULT NULL, openId VARCHAR(255) DEFAULT NULL, accountName VARCHAR(255) DEFAULT NULL, accountHomePage VARCHAR(255) DEFAULT NULL, name VARCHAR(255) DEFAULT NULL, members VARCHAR(255) NOT NULL, PRIMARY KEY(identifier)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB;
CREATE TABLE xapi_statement (id VARCHAR(255) NOT NULL, actor_id INT DEFAULT NULL, verb_id INT DEFAULT NULL, object_id INT DEFAULT NULL, result_id INT DEFAULT NULL, authority_id INT DEFAULT NULL, context_id INT DEFAULT NULL, created BIGINT DEFAULT NULL, `stored` BIGINT DEFAULT NULL, hasAttachments TINYINT(1) DEFAULT NULL, UNIQUE INDEX UNIQ_BAF6663B10DAF24A (actor_id), UNIQUE INDEX UNIQ_BAF6663BC1D03483 (verb_id), UNIQUE INDEX UNIQ_BAF6663B232D562B (object_id), UNIQUE INDEX UNIQ_BAF6663B7A7B643 (result_id), UNIQUE INDEX UNIQ_BAF6663B81EC865B (authority_id), UNIQUE INDEX UNIQ_BAF6663B6B00C1CF (context_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB;
ALTER TABLE xapi_attachment ADD CONSTRAINT FK_7148C9A1849CB65B FOREIGN KEY (statement_id) REFERENCES xapi_statement (id);
ALTER TABLE xapi_object ADD CONSTRAINT FK_E2B68640303C7F1D FOREIGN KEY (activityExtensions_id) REFERENCES xapi_extensions (identifier);
ALTER TABLE xapi_object ADD CONSTRAINT FK_E2B68640FE54D947 FOREIGN KEY (group_id) REFERENCES xapi_object (identifier);
ALTER TABLE xapi_object ADD CONSTRAINT FK_E2B6864010DAF24A FOREIGN KEY (actor_id) REFERENCES xapi_object (identifier);
ALTER TABLE xapi_object ADD CONSTRAINT FK_E2B68640C1D03483 FOREIGN KEY (verb_id) REFERENCES xapi_verb (identifier);
ALTER TABLE xapi_object ADD CONSTRAINT FK_E2B68640232D562B FOREIGN KEY (object_id) REFERENCES xapi_object (identifier);
ALTER TABLE xapi_object ADD CONSTRAINT FK_E2B68640988A4CEC FOREIGN KEY (parentContext_id) REFERENCES xapi_context (identifier);
ALTER TABLE xapi_object ADD CONSTRAINT FK_E2B686404F542860 FOREIGN KEY (groupingContext_id) REFERENCES xapi_context (identifier);
ALTER TABLE xapi_object ADD CONSTRAINT FK_E2B68640AEA1B132 FOREIGN KEY (categoryContext_id) REFERENCES xapi_context (identifier);
ALTER TABLE xapi_object ADD CONSTRAINT FK_E2B68640B73EEAB7 FOREIGN KEY (otherContext_id) REFERENCES xapi_context (identifier);
ALTER TABLE xapi_result ADD CONSTRAINT FK_5971ECBFD0A19400 FOREIGN KEY (extensions_id) REFERENCES xapi_extensions (identifier);
ALTER TABLE xapi_context ADD CONSTRAINT FK_3D7771908C4FC193 FOREIGN KEY (instructor_id) REFERENCES xapi_object (identifier);
ALTER TABLE xapi_context ADD CONSTRAINT FK_3D777190296CD8AE FOREIGN KEY (team_id) REFERENCES xapi_object (identifier);
ALTER TABLE xapi_context ADD CONSTRAINT FK_3D777190D0A19400 FOREIGN KEY (extensions_id) REFERENCES xapi_extensions (identifier);
ALTER TABLE xapi_statement ADD CONSTRAINT FK_BAF6663B10DAF24A FOREIGN KEY (actor_id) REFERENCES xapi_object (identifier);
ALTER TABLE xapi_statement ADD CONSTRAINT FK_BAF6663BC1D03483 FOREIGN KEY (verb_id) REFERENCES xapi_verb (identifier);
ALTER TABLE xapi_statement ADD CONSTRAINT FK_BAF6663B232D562B FOREIGN KEY (object_id) REFERENCES xapi_object (identifier);
ALTER TABLE xapi_statement ADD CONSTRAINT FK_BAF6663B7A7B643 FOREIGN KEY (result_id) REFERENCES xapi_result (identifier);
ALTER TABLE xapi_statement ADD CONSTRAINT FK_BAF6663B81EC865B FOREIGN KEY (authority_id) REFERENCES xapi_object (identifier);
ALTER TABLE xapi_statement ADD CONSTRAINT FK_BAF6663B6B00C1CF FOREIGN KEY (context_id) REFERENCES xapi_context (identifier);
CREATE TABLE xapi_shared_statement (id INT AUTO_INCREMENT NOT NULL, uuid VARCHAR(255) DEFAULT NULL, statement LONGTEXT NOT NULL COMMENT '(DC2Type:array)', sent TINYINT(1) DEFAULT '0' NOT NULL, INDEX idx_uuid (uuid), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB;
CREATE TABLE xapi_lrs_auth (id INT AUTO_INCREMENT NOT NULL, username VARCHAR(255) NOT NULL, password VARCHAR(255) NOT NULL, enabled TINYINT(1) NOT NULL, created_at DATETIME NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB;
CREATE TABLE xapi_tool_launch (id INT AUTO_INCREMENT NOT NULL, c_id INT NOT NULL, session_id INT DEFAULT NULL, title VARCHAR(255) NOT NULL, description LONGTEXT DEFAULT NULL, launch_url VARCHAR(255) NOT NULL, activity_id VARCHAR(255) DEFAULT NULL, activity_type VARCHAR(255) DEFAULT NULL, allow_multiple_attempts TINYINT(1) DEFAULT '1' NOT NULL, lrs_url VARCHAR(255) DEFAULT NULL, lrs_auth_username VARCHAR(255) DEFAULT NULL, lrs_auth_password VARCHAR(255) DEFAULT NULL, INDEX IDX_E18CB58391D79BD3 (c_id), INDEX IDX_E18CB583613FECDF (session_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB;
ALTER TABLE xapi_tool_launch ADD CONSTRAINT FK_E18CB58391D79BD3 FOREIGN KEY (c_id) REFERENCES course (id);
ALTER TABLE xapi_tool_launch ADD CONSTRAINT FK_E18CB583613FECDF FOREIGN KEY (session_id) REFERENCES session (id);
CREATE TABLE xapi_cmi5_item (id INT AUTO_INCREMENT NOT NULL, tree_root INT DEFAULT NULL, parent_id INT DEFAULT NULL, identifier VARCHAR(255) NOT NULL, type VARCHAR(255) NOT NULL, title LONGTEXT NOT NULL COMMENT '(DC2Type:json)', description LONGTEXT NOT NULL COMMENT '(DC2Type:json)', url VARCHAR(255) DEFAULT NULL, activity_type VARCHAR(255) DEFAULT NULL, launch_method VARCHAR(255) DEFAULT NULL, move_on VARCHAR(255) DEFAULT NULL, mastery_score DOUBLE PRECISION DEFAULT NULL, launch_parameters VARCHAR(255) DEFAULT NULL, entitlement_key VARCHAR(255) DEFAULT NULL, status VARCHAR(255) DEFAULT NULL, lft INT NOT NULL, lvl INT NOT NULL, rgt INT NOT NULL, tool_id INT DEFAULT NULL, INDEX IDX_7CA116D88F7B22CC (tool_id), INDEX IDX_7CA116D8A977936C (tree_root), INDEX IDX_7CA116D8727ACA70 (parent_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB;
ALTER TABLE xapi_cmi5_item ADD CONSTRAINT FK_7CA116D8A977936C FOREIGN KEY (tree_root) REFERENCES xapi_cmi5_item (id) ON DELETE CASCADE;
ALTER TABLE xapi_cmi5_item ADD CONSTRAINT FK_7CA116D8727ACA70 FOREIGN KEY (parent_id) REFERENCES xapi_cmi5_item (id) ON DELETE CASCADE;
CREATE TABLE xapi_activity_state (id INT AUTO_INCREMENT NOT NULL, state_id VARCHAR(255) NOT NULL, activity_id VARCHAR(255) NOT NULL, agent LONGTEXT NOT NULL COMMENT '(DC2Type:json)', document_data LONGTEXT NOT NULL COMMENT '(DC2Type:json)', PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB;
CREATE TABLE xapi_activity_profile (id INT AUTO_INCREMENT NOT NULL, profile_id VARCHAR(255) NOT NULL, activity_id VARCHAR(255) NOT NULL, document_data LONGTEXT NOT NULL COMMENT '(DC2Type:json)', PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB;
```
**From 0.2 (beta) [2021-10-15]**
- With the LRS an internal log is registered based on the actor mbox's email or the actor account's name coming from the statement
To update, execute this queries:
```sql
CREATE TABLE xapi_internal_log (id INT AUTO_INCREMENT NOT NULL, user_id INT DEFAULT NULL, statement_id VARCHAR(255) NOT NULL, verb VARCHAR(255) NOT NULL, object_id VARCHAR(255) NOT NULL, activity_name VARCHAR(255) DEFAULT NULL, activity_description VARCHAR(255) DEFAULT NULL, score_scaled DOUBLE PRECISION DEFAULT NULL, score_raw DOUBLE PRECISION DEFAULT NULL, score_min DOUBLE PRECISION DEFAULT NULL, score_max DOUBLE PRECISION DEFAULT NULL, created_at DATETIME DEFAULT NULL, INDEX IDX_C1C667ACA76ED395 (user_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB;
ALTER TABLE xapi_internal_log ADD CONSTRAINT FK_C1C667ACA76ED395 FOREIGN KEY (user_id) REFERENCES user (id);
```
**From 0.3 (beta) [2021-11-11]**
- Fix: Add foreign keys with course/session in tool_launch table and foreign key with user in internal_log table.
```sql
ALTER TABLE xapi_internal_log ADD CONSTRAINT FK_C1C667ACA76ED395 FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE;
ALTER TABLE xapi_tool_launch ADD CONSTRAINT FK_E18CB58391D79BD3 FOREIGN KEY (c_id) REFERENCES course (id) ON DELETE CASCADE;
ALTER TABLE xapi_tool_launch ADD CONSTRAINT FK_E18CB583613FECDF FOREIGN KEY (session_id) REFERENCES session (id) ON DELETE CASCADE;
```

196
plugin/xapi/admin.php Normal file
View File

@@ -0,0 +1,196 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\PluginBundle\Entity\XApi\LrsAuth;
$cidReset = true;
require_once __DIR__.'/../../main/inc/global.inc.php';
api_protect_admin_script();
$request = \Symfony\Component\HttpFoundation\Request::createFromGlobals();
$plugin = XApiPlugin::create();
$em = Database::getManager();
$pageBaseUrl = api_get_self();
$pageActions = '';
$pageContent = '';
/**
* @throws \Exception
*
* @return \FormValidator
*/
function createForm(LrsAuth $auth = null)
{
$pageBaseUrl = api_get_self();
$action = $pageBaseUrl.'?action=add';
if (null != $auth) {
$action = $pageBaseUrl."?action=edit&id={$auth->getId()}";
}
$form = new FormValidator('frm_xapi_auth', 'post', $action);
$form->addText('username', get_lang('Username'), true);
$form->addText('password', get_lang('Password'), true);
$form->addCheckBox('enabled', get_lang('Enabled'), get_lang('Yes'));
$form->addButtonSave(get_lang('Save'));
if (null != $auth) {
$form->setDefaults(
[
'username' => $auth->getUsername(),
'password' => $auth->getPassword(),
'enabled' => $auth->isEnabled(),
]
);
}
return $form;
}
switch ($request->query->getAlpha('action')) {
case 'add':
$form = createForm();
if ($form->validate()) {
$values = $form->exportValues();
$auth = new LrsAuth();
$auth
->setUsername($values['username'])
->setPassword($values['password'])
->setEnabled(isset($values['enabled']))
->setCreatedAt(
api_get_utc_datetime(null, false, true)
);
$em->persist($auth);
$em->flush();
Display::addFlash(
Display::return_message(get_lang('ItemAdded'), 'success')
);
header('Location: '.$pageBaseUrl);
exit;
}
$pageActions = Display::url(
Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
$pageBaseUrl
);
$pageContent = $form->returnForm();
break;
case 'edit':
$auth = $em->find(LrsAuth::class, $request->query->getInt('id'));
if (null == $auth) {
api_not_allowed(true);
}
$form = createForm($auth);
if ($form->validate()) {
$values = $form->exportValues();
$auth
->setUsername($values['username'])
->setPassword($values['password'])
->setEnabled(isset($values['enabled']))
->setCreatedAt(
api_get_utc_datetime(null, false, true)
);
$em->persist($auth);
$em->flush();
Display::addFlash(
Display::return_message(get_lang('ItemUpdated'), 'success')
);
header('Location: '.$pageBaseUrl);
exit;
}
$pageActions = Display::url(
Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
$pageBaseUrl
);
$pageContent = $form->returnForm();
break;
case 'delete':
$auth = $em->find(LrsAuth::class, $request->query->getInt('id'));
if (null == $auth) {
api_not_allowed(true);
}
$em->remove($auth);
$em->flush();
Display::addFlash(
Display::return_message(get_lang('ItemDeleted'), 'success')
);
header('Location: '.$pageBaseUrl);
exit;
case 'list':
default:
$pageActions = Display::url(
Display::return_icon('add.png', get_lang('Add'), [], ICON_SIZE_MEDIUM),
$pageBaseUrl.'?action=add'
);
$pageContent = Display::return_message(get_lang('NoData'), 'warning');
$auths = $em->getRepository(LrsAuth::class)->findAll();
if (count($auths) > 0) {
$row = 0;
$table = new HTML_Table(['class' => 'table table-striped table-hover']);
$table->setHeaderContents($row, 0, get_lang('Username'));
$table->setHeaderContents($row, 1, get_lang('Password'));
$table->setHeaderContents($row, 2, get_lang('Enabled'));
$table->setHeaderContents($row, 3, get_lang('CreatedAt'));
$table->setHeaderContents($row, 4, get_lang('Actions'));
foreach ($auths as $auth) {
$row++;
$actions = [
Display::url(
Display::return_icon('edit.png', get_lang('Edit')),
$pageBaseUrl.'?action=edit&id='.$auth->getId()
),
Display::url(
Display::return_icon('delete.png', get_lang('Edit')),
$pageBaseUrl.'?action=delete&id='.$auth->getId()
),
];
$table->setCellContents($row, 0, $auth->getUsername());
$table->setCellContents($row, 1, $auth->getPassword());
$table->setCellContents($row, 2, $auth->isEnabled() ? get_lang('Yes') : get_lang('No'));
$table->setCellContents($row, 3, api_convert_and_format_date($auth->getCreatedAt()));
$table->setCellContents($row, 4, implode(PHP_EOL, $actions));
}
$pageContent = $table->toHtml();
}
break;
}
$interbreadcrumb[] = [
'name' => get_lang('Administration'),
'url' => api_get_path(WEB_CODE_PATH).'admin/index.php',
];
$view = new Template($plugin->get_title());
$view->assign('actions', Display::toolbarAction('xapi_actions', [$pageActions]));
$view->assign('content', $pageContent);
$view->display_one_col_template();

View File

@@ -0,0 +1,40 @@
.section-global {
margin: 0;
}
#pnl-left ul {
list-style: none;
margin: 0;
padding: 0;
}
#pnl-left ul li a {
padding: 5px 0;
display: block;
}
#pnl-left ul ul li {
padding: 0 0 0 15px ;
}
@media (min-width: 992px) {
#pnl-left {
overflow: auto;
position: absolute;
top: 0;
bottom: 0;
left: 0;
}
#pnl-right {
position: absolute;
right: 0;
top: 0;
bottom: 0;
}
#ifr-content {
min-width: 100%;
min-height: 100%;
padding: 0;
margin: 0;
position: absolute;
right: 0;
}
}

View File

@@ -0,0 +1,2 @@
$(function () {
});

188
plugin/xapi/cmi5/launch.php Normal file
View File

@@ -0,0 +1,188 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\PluginBundle\Entity\XApi\Cmi5Item;
use Symfony\Component\HttpFoundation\Request as HttpRequest;
use Xabbuh\XApi\Model\Account;
use Xabbuh\XApi\Model\Activity;
use Xabbuh\XApi\Model\Agent;
use Xabbuh\XApi\Model\Context;
use Xabbuh\XApi\Model\Definition;
use Xabbuh\XApi\Model\DocumentData;
use Xabbuh\XApi\Model\InverseFunctionalIdentifier;
use Xabbuh\XApi\Model\IRI;
use Xabbuh\XApi\Model\IRL;
use Xabbuh\XApi\Model\LanguageMap;
use Xabbuh\XApi\Model\State;
use Xabbuh\XApi\Model\StateDocument;
use Xabbuh\XApi\Model\Statement;
use Xabbuh\XApi\Model\StatementId;
use Xabbuh\XApi\Model\Uuid;
use Xabbuh\XApi\Model\Verb;
require_once __DIR__.'/../../../main/inc/global.inc.php';
api_protect_course_script(true);
api_block_anonymous_users();
$request = HttpRequest::createFromGlobals();
$em = Database::getManager();
$item = $em->find(Cmi5Item::class, $request->query->getInt('id'));
$toolLaunch = $item->getTool();
if ($toolLaunch->getId() !== $request->query->getInt('tool')) {
api_not_allowed(
false,
Display::return_message(get_lang('NotAllwed'), 'error')
);
}
$plugin = XApiPlugin::create();
$user = api_get_user_entity(api_get_user_id());
$nowDate = api_get_utc_datetime(null, false, true)->format('c');
$registration = (string) Uuid::uuid4();
$actor = new Agent(
InverseFunctionalIdentifier::withAccount(
new Account(
$user->getCompleteName(),
IRL::fromString(api_get_path(WEB_PATH))
)
),
$user->getCompleteName()
);
$verb = new Verb(
IRI::fromString('http://adlnet.gov/expapi/verbs/launched'),
LanguageMap::create($plugin->getLangMap('Launched'))
);
$customActivityId = $plugin->generateIri($item->getId(), 'cmi5_item');
$activity = new Activity(
$customActivityId,
new Definition(
LanguageMap::create($item->getTitle()),
LanguageMap::create($item->getDescription()),
IRI::fromString($item->getIdentifier())
)
);
$context = (new Context())
->withPlatform(
api_get_setting('Institution').' - '.api_get_setting('siteName')
)
->withLanguage(api_get_language_isocode())
->withRegistration($registration);
$statementUuid = Uuid::uuid5(
$plugin->get(XApiPlugin::SETTING_UUID_NAMESPACE),
"cmi5_item/{$item->getId()}"
);
$statement = new Statement(
StatementId::fromUuid($statementUuid),
$actor,
$verb,
$activity,
null,
null,
api_get_utc_datetime(null, false, true),
null,
$context
);
$statementClient = XApiPlugin::create()->getXApiStatementClient();
//try {
// $statementClient->storeStatement($statement);
//} catch (ConflictException $e) {
// echo Display::return_message($e->getMessage(), 'error');
//
// exit;
//} catch (XApiException $e) {
// echo Display::return_message($e->getMessage(), 'error');
//
// exit;
//}
$viewSessionId = (string) Uuid::uuid4();
$state = new State(
$activity,
$actor,
'LMS.LaunchData',
(string) $registration
);
$documentDataData = [];
$documentDataData['contentTemplate'] = [
'extensions' => [
'https://w3id.org/xapi/cmi5/context/extensions/sessionid' => $viewSessionId,
],
];
$documentDataData['launchMode'] = 'Normal';
$documentDataData['launchMethod'] = $item->getLaunchMethod();
if ($item->getLaunchParameters()) {
$documentDataData['launchParameteres'] = $item->getLaunchParameters();
}
if ($item->getMasteryScore()) {
$documentDataData['masteryScore'] = $item->getMasteryScore();
}
if ($item->getEntitlementKey()) {
$documentDataData['entitlementKey'] = [
'courseStructure' => $item->getEntitlementKey(),
];
}
$documentData = new DocumentData($documentDataData);
try {
$plugin
->getXApiStateClient()
->createOrReplaceDocument(
new StateDocument($state, $documentData)
);
} catch (Exception $exception) {
echo Display::return_message($exception->getMessage(), 'error');
exit;
}
$launchUrl = $plugin->generateLaunchUrl(
'cmi5',
$item->getUrl(),
$customActivityId->getValue(),
$actor,
$registration,
$toolLaunch->getLrsUrl(),
$toolLaunch->getLrsAuthUsername(),
$toolLaunch->getLrsAuthPassword(),
$viewSessionId
);
if ('OwnWindow' === $item->getLaunchMethod()) {
Display::display_reduced_header();
echo '<br><p class="text-center">';
echo Display::toolbarButton(
$plugin->get_lang('LaunchNewAttempt'),
$launchUrl,
'external-link fa-fw',
'success',
[
'target' => '_blank',
]
);
echo '</div>';
Display::display_reduced_footer();
exit;
}
header("Location: $launchUrl");

View File

@@ -0,0 +1,22 @@
<?php
/* For licensing terms, see /license.txt */
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request as HttpRequest;
use Symfony\Component\HttpFoundation\Response;
require_once __DIR__.'/../../../main/inc/global.inc.php';
$request = HttpRequest::createFromGlobals();
$response = new JsonResponse([], Response::HTTP_METHOD_NOT_ALLOWED);
if ('POST' === $request->getMethod()) {
$token = base64_encode(uniqid());
$response->setStatusCode(Response::HTTP_OK);
$response->setData(['auth-token' => $token]);
}
$response->send();

84
plugin/xapi/cmi5/view.php Normal file
View File

@@ -0,0 +1,84 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\PluginBundle\Entity\XApi\Cmi5Item;
use Chamilo\PluginBundle\Entity\XApi\ToolLaunch;
use Symfony\Component\HttpFoundation\Request as HttpRequest;
use Xabbuh\XApi\Model\LanguageMap;
require_once __DIR__.'/../../../main/inc/global.inc.php';
api_protect_course_script(true);
api_block_anonymous_users();
$request = HttpRequest::createFromGlobals();
$em = Database::getManager();
$toolLaunch = $em->find(
ToolLaunch::class,
$request->query->getInt('id')
);
if (null === $toolLaunch
|| 'cmi5' !== $toolLaunch->getActivityType()
) {
header('Location: '.api_get_course_url());
exit;
}
$plugin = XApiPlugin::create();
$course = api_get_course_entity();
$session = api_get_session_entity();
$cidReq = api_get_cidreq();
$user = api_get_user_entity(api_get_user_id());
$interfaceLanguage = api_get_interface_language();
$itemsRepo = $em->getRepository(Cmi5Item::class);
$query = $em->createQueryBuilder()
->select('item')
->from(Cmi5Item::class, 'item')
->where('item.tool = :tool')
->setParameter('tool', $toolLaunch->getId())
->getQuery();
$tocHtml = $itemsRepo->buildTree(
$query->getArrayResult(),
[
'decorate' => true,
'rootOpen' => '<ul>',
'rootClose' => '</ul>',
'childOpen' => '<li>',
'childClose' => '</li>',
'nodeDecorator' => function ($node) use ($interfaceLanguage, $cidReq, $toolLaunch) {
$titleMap = LanguageMap::create($node['title']);
$title = XApiPlugin::extractVerbInLanguage($titleMap, $interfaceLanguage);
if ('block' === $node['type']) {
return Display::page_subheader($title, null, 'h4');
}
return Display::url(
$title,
"launch.php?tool={$toolLaunch->getId()}&id={$node['id']}&$cidReq",
[
'target' => 'ifr_content',
'class' => 'text-left btn-link',
]
);
},
]
);
$webPluginPath = api_get_path(WEB_PLUGIN_PATH);
$htmlHeadXtra[] = api_get_css($webPluginPath.'xapi/assets/css/cmi5_launch.css');
$htmlHeadXtra[] = api_get_js_simple($webPluginPath.'xapi/assets/js/cmi5_launch.js');
$view = new Template('', false, false, true, true, false);
$view->assign('tool', $toolLaunch);
$view->assign('toc_html', $tocHtml);
$view->assign('content', $view->fetch('xapi/views/cmi5_launch.twig'));
$view->display_no_layout_template();

View File

@@ -0,0 +1,79 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\PluginBundle\Entity\XApi\SharedStatement;
use Xabbuh\XApi\Common\Exception\ConflictException;
use Xabbuh\XApi\Common\Exception\XApiException;
use Xabbuh\XApi\Model\StatementId;
use Xabbuh\XApi\Model\Uuid;
use Xabbuh\XApi\Serializer\Symfony\Serializer;
use Xabbuh\XApi\Serializer\Symfony\StatementSerializer;
require_once __DIR__.'/../../../main/inc/global.inc.php';
if (php_sapi_name() !== 'cli') {
exit;
}
echo 'XAPI: Cron to send statements.'.PHP_EOL;
$em = Database::getManager();
$serializer = Serializer::createSerializer();
$statementSerializer = new StatementSerializer($serializer);
$notSentSharedStatements = $em
->getRepository(SharedStatement::class)
->findBy(
['uuid' => null, 'sent' => false],
null,
100
);
$countNotSent = count($notSentSharedStatements);
if ($countNotSent > 0) {
echo '['.time().'] Trying to send '.$countNotSent.' statements to LRS'.PHP_EOL;
$client = XApiPlugin::create()->getXapiStatementCronClient();
/** @var SharedStatement $notSentSharedStatement */
foreach ($notSentSharedStatements as $notSentSharedStatement) {
$notSentStatement = $statementSerializer->deserializeStatement(
json_encode($notSentSharedStatement->getStatement())
);
if (null == $notSentStatement->getId()) {
$notSentStatement = $notSentStatement->withId(
StatementId::fromUuid(Uuid::uuid4())
);
}
try {
echo '['.time()."] Sending shared statement ({$notSentSharedStatement->getId()})";
$sentStatement = $client->storeStatement($notSentStatement);
echo "\t\tStatement ID received: \"{$sentStatement->getId()->getValue()}\"";
} catch (ConflictException $e) {
echo $e->getMessage().PHP_EOL;
continue;
} catch (XApiException $e) {
echo $e->getMessage().PHP_EOL;
continue;
}
$notSentSharedStatement
->setUuid($sentStatement->getId()->getValue())
->setSent(true);
$em->persist($notSentSharedStatement);
echo "\t\tShared statement updated".PHP_EOL;
}
$em->flush();
} else {
echo 'No statements to process.'.PHP_EOL;
}

5
plugin/xapi/install.php Normal file
View File

@@ -0,0 +1,5 @@
<?php
/* For licensing terms, see /license.txt */
XApiPlugin::create()->install();

View File

@@ -0,0 +1,56 @@
<?php
/* For licensing terms, see /license.txt */
$strings['plugin_title'] = 'Experience API (xAPI)';
$strings['plugin_comment'] = 'Allows you to connect to an external (or internal) Learning Record Store and use activities compatible with the xAPI standard.';
$strings[XApiPlugin::SETTING_UUID_NAMESPACE] = 'UUID Namespace';
$strings[XApiPlugin::SETTING_UUID_NAMESPACE.'_help'] = 'Namespace for universally unique identifiers used as statement IDs.'
.'<br>This is generated automatically by Chamilo LMS. <strong>Don\'t replace it.</strong>';
$strings['lrs_url'] = 'LRS endpoint';
$strings['lrs_url_help'] = 'Base URL of the LRS';
$strings['lrs_auth_username'] = 'LRS user';
$strings['lrs_auth_username_help'] = 'Username for basic HTTP authentication';
$strings['lrs_auth_password'] = 'LRS password';
$strings['lrs_auth_password_help'] = 'Password for basic HTTP authentication';
$strings['cron_lrs_url'] = 'Cron: LRS endpoint';
$strings['cron_lrs_url_help'] = 'Alternative base URL of the LRS for the cron process';
$strings['cron_lrs_auth_username'] = 'Cron: LRS user';
$strings['cron_lrs_auth_username_help'] = 'Alternative username for basic HTTP authentication for the cron process';
$strings['cron_lrs_auth_password'] = 'Cron: LRS password';
$strings['cron_lrs_auth_password_help'] = 'Alternative password for basic HTTP authentication for the cron process';
$strings['lrs_lp_item_viewed_active'] = 'Learning path item viewed';
$strings['lrs_lp_end_active'] = 'Learning path ended';
$strings['lrs_quiz_active'] = 'Quiz ended';
$strings['lrs_quiz_question_active'] = 'Quiz question answered';
$strings['lrs_portfolio_active'] = 'Portfolio events';
$strings['NoActivities'] = 'No activities added yet';
$strings['ActivityTitle'] = 'Activity';
$strings['AddActivity'] = 'Add activity';
$strings['TinCanPackage'] = 'TinCan package (zip)';
$strings['Cmi5Package'] = 'Cmi5 package (zip)';
$strings['OnlyZipAllowed'] = 'Only ZIP file allowed (.zip).';
$strings['ActivityImported'] = 'Activity imported.';
$strings['EditActivity'] = 'Edit activity';
$strings['ActivityUpdated'] = 'Activity updated';
$strings['ActivityLaunchUrl'] = 'Launch URL';
$strings['ActivityId'] = 'Activity ID';
$strings['ActivityType'] = 'Activity type';
$strings['ActivityDeleted'] = 'Activity deleted';
$strings['ActivityLaunch'] = 'Launch';
$strings['ActivityFirstLaunch'] = 'First launch at';
$strings['ActivityLastLaunch'] = 'Last launch at';
$strings['LaunchNewAttempt'] = 'Launch new attempt';
$strings['LrsConfiguration'] = 'LRS Configuration';
$strings['Verb'] = 'Verb';
$strings['Actor'] = 'Actor';
$strings['ToolTinCan'] = 'Activities';
$strings['Terminated'] = 'Terminated';
$strings['Completed'] = 'Completed';
$strings['Answered'] = 'Answered';
$strings['Viewed'] = 'Viewed';
$strings['ActivityAddedToLPCannotBeAccessed'] = 'This activity has been included in a learning path, so it cannot be accessed by students directly from here.';
$strings['XApiPackage'] = 'XApi Package';
$strings['TinCanAllowMultipleAttempts'] = 'Allow multiple attempts';

View File

@@ -0,0 +1,49 @@
<?php
/* For licensing terms, see /license.txt */
$strings['plugin_title'] = 'Experience API (xAPI)';
$strings['plugin_comment'] = 'Permet l\'intégration d\'un Learning Record Store (interne ou externe) et de clients xAPI';
$strings[XApiPlugin::SETTING_UUID_NAMESPACE] = 'Namespace UUID';
$strings[XApiPlugin::SETTING_UUID_NAMESPACE.'_help'] = 'Namespace pour identifiants uniques universels qui servent comme IDs de déclaration xAPI.'
.'<br>Cette valeur est générée automatiquement par Chamilo, <strong>ne la modifiez pas.</strong>';
$strings['lrs_url'] = 'Point d\'entrée LRS';
$strings['lrs_url_help'] = 'URL de base du LRS';
$strings['lrs_auth_username'] = 'Utilisateur LRS';
$strings['lrs_auth_username_help'] = 'Nom d\'utilisateur pour l\'authentification HTTP de base';
$strings['lrs_auth_password'] = 'Mot de passe LRS';
$strings['lrs_auth_password_help'] = 'Mot de passe pour l\'authentification HTTP de base';
$strings['cron_lrs_url'] = 'Cron: LRS endpoint';
$strings['cron_lrs_url_help'] = 'Alternative base URL of the LRS for the cron process';
$strings['cron_lrs_auth_username'] = 'Cron: LRS user';
$strings['cron_lrs_auth_username_help'] = 'Alternative username for basic HTTP authentication for the cron process';
$strings['cron_lrs_auth_password'] = 'Cron: LRS password';
$strings['cron_lrs_auth_password_help'] = 'Alternative password for basic HTTP authentication for the cron process';
$strings['lrs_lp_item_viewed_active'] = 'Élément de parcours visionné';
$strings['lrs_lp_end_active'] = 'Parcours terminé';
$strings['lrs_quiz_active'] = 'Exercice terminé';
$strings['lrs_quiz_question_active'] = 'Question d\'exercice répondue';
$strings['lrs_portfolio_active'] = 'Événements de portfolio';
$strings['NoActivities'] = 'Aucune activité ajoutée pour l\'instant';
$strings['ActivityTitle'] = 'Activité';
$strings['AddActivity'] = 'Ajouter activité';
$strings['TinCanPackage'] = 'Paquet TinCan (zip)';
$strings['OnlyZipAllowed'] = 'Seuls les fichiers ZIP sont autorisés (.zip).';
$strings['ActivityImported'] = 'Activité importée.';
$strings['EditActivity'] = 'Éditer activité';
$strings['ActivityUpdated'] = 'Activité mise à jour';
$strings['ActivityLaunchUrl'] = 'URL de lancement';
$strings['ActivityId'] = 'ID d\'activité';
$strings['ActivityType'] = 'Type d\'activité';
$strings['ActivityDeleted'] = 'Activité supprimée';
$strings['ActivityLaunch'] = 'Lancer';
$strings['ActivityFirstLaunch'] = 'Premier lancement à';
$strings['ActivityLastLaunch'] = 'Dernier lancement à';
$strings['LaunchNewAttempt'] = 'Lancer nouvelle tentative';
$strings['LrsConfiguration'] = 'Configuration LRS';
$strings['Verb'] = 'Verbe';
$strings['Actor'] = 'Acteur';
$strings['ToolTinCan'] = 'Activités';
$strings['ActivityAddedToLPCannotBeAccessed'] = 'Cet activité fait partie d\'un parcours d\'apprentissage, il n\'est donc pas accessible par les étudiants depuis cette page';

View File

@@ -0,0 +1,56 @@
<?php
/* For licensing terms, see /license.txt */
$strings['plugin_title'] = 'Experience API (xAPI)';
$strings['plugin_comment'] = 'Permite incorporar un Learning Record Store externo (o interno) y usar actividades con la especificación xAPI.';
$strings[XApiPlugin::SETTING_UUID_NAMESPACE] = 'UUID Namespace';
$strings[XApiPlugin::SETTING_UUID_NAMESPACE.'_help'] = 'Namespace para los identificadores unicos universales (UUID) usados como IDs de statements.'
.'<br>Esto es generado automáticamente por Chamilo LMS. <strong>No reemplazarlo.</strong>';
$strings['lrs_url'] = 'LRS endpoint';
$strings['lrs_url_help'] = 'Base de la URL del LRS';
$strings['lrs_auth_username'] = 'Usuario del LRS';
$strings['lrs_auth_username_help'] = 'Usuario para autenticación con HTTP básica';
$strings['lrs_auth_password'] = 'Contraseña del LRS';
$strings['lrs_auth_password_help'] = 'Contraseña para autenticación con HTTP básica';
$strings['cron_lrs_url'] = 'Cron: LRS endpoint';
$strings['cron_lrs_url_help'] = 'Opcional. Base de la URL alternativa del LRS del proceso cron.';
$strings['cron_lrs_auth_username'] = 'Cron: Usuario del LRS';
$strings['cron_lrs_auth_username_help'] = 'Opcional. Usuario alternativo para autenticación con HTTP básica del proceso cron';
$strings['cron_lrs_auth_password'] = 'Cron: Contraseña del LRS';
$strings['cron_lrs_auth_password_help'] = 'Opcional. Contraseña alternativa para autenticación con HTTP básica del proceso cron';
$strings['lrs_lp_item_viewed_active'] = 'Visualización de contenido de lección';
$strings['lrs_lp_end_active'] = 'Finalización de lección';
$strings['lrs_quiz_active'] = 'Finalización de ejercicio';
$strings['lrs_quiz_question_active'] = 'Resolución de pregunta en ejercicio';
$strings['lrs_portfolio_active'] = 'Eventos en portafolio';
$strings['NoActivities'] = 'No hay actividades aún';
$strings['ActivityTitle'] = 'Actividad';
$strings['AddActivity'] = 'Agregar actividad';
$strings['TinCanPackage'] = 'Paquete TinCan (zip)';
$strings['Cmi5Package'] = 'Paquete Cmi5(zip)';
$strings['OnlyZipAllowed'] = 'Sólo archivos ZIP están permitidos (.zip).';
$strings['ActivityImported'] = 'Actividad importada.';
$strings['EditActivity'] = 'Editar actividad';
$strings['ActivityUpdated'] = 'Actividad actualizada';
$strings['ActivityLaunchUrl'] = 'URL de inicio';
$strings['ActivityId'] = 'ID de actividad';
$strings['ActivityType'] = 'Tipo de actividad';
$strings['ActivityDeleted'] = 'Actividad eliminada';
$strings['ActivityLaunch'] = 'Iniciar';
$strings['ActivityFirstLaunch'] = 'Primer inicio';
$strings['ActivityLastLaunch'] = 'Últimmo inicio';
$strings['LaunchNewAttempt'] = 'Iniciar nuevo intento';
$strings['LrsConfiguration'] = 'Configuración de LRS';
$strings['Verb'] = 'Verbo';
$strings['Actor'] = 'Actor';
$strings['ToolTinCan'] = 'Actividades';
$strings['Terminated'] = 'Terminó';
$strings['Completed'] = 'Completó';
$strings['Answered'] = 'Respondió';
$strings['Viewed'] = 'Visualizó';
$strings['ActivityAddedToLPCannotBeAccessed'] = 'Esta actividad ha sido incluida en una secuencia de aprendizaje, por lo cual no podrá ser accesible directamente por los estudiantes desde aquí.';
$strings['XApiPackage'] = 'Paquete XApi';
$strings['TinCanAllowMultipleAttempts'] = 'Permitir múltiples intentos';

12
plugin/xapi/lrs.php Normal file
View File

@@ -0,0 +1,12 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\PluginBundle\XApi\Lrs\LrsRequest;
$cidReset = true;
require_once __DIR__.'/../../main/inc/global.inc.php';
$lrsRequest = new LrsRequest();
$lrsRequest->send();

View File

@@ -0,0 +1,19 @@
Copyright (c) 2016-2017 Christian Flothmann
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,51 @@
{
"name": "php-xapi/lrs-bundle",
"type": "symfony-bundle",
"description": "Experience API (xAPI) Learning Record Store (LRS) based on the Symfony Framework",
"keywords": ["xAPI", "Experience API", "Tin Can API", "LRS", "Learning Record Store", "Symfony", "bundle"],
"homepage": "https://github.com/php-xapi/lrs-bundle",
"license": "MIT",
"authors": [
{
"name": "Christian Flothmann",
"homepage": "https://github.com/xabbuh"
},
{
"name": "Jérôme Parmentier",
"homepage": "https://github.com/Lctrs"
}
],
"require": {
"php": "^7.1",
"php-xapi/exception": "^0.1 || ^0.2",
"php-xapi/model": "^1.1 || ^2.0 || ^3.0",
"php-xapi/repository-api": "^0.3@dev || ^0.4@dev",
"php-xapi/serializer": "^1.0 || ^2.0",
"php-xapi/symfony-serializer": "^1.0 || ^2.0",
"symfony/config": "^3.4 || ^4.3",
"symfony/dependency-injection": "^3.4 || ^4.3",
"symfony/http-foundation": "^3.4 || ^4.3",
"symfony/http-kernel": "^3.4 || ^4.3"
},
"require-dev": {
"phpspec/phpspec": "~2.3",
"php-xapi/json-test-fixtures": "^1.0 || ^2.0",
"php-xapi/test-fixtures": "^1.0.1",
"ramsey/uuid": "^2.9 || ^3.0"
},
"autoload": {
"psr-4": {
"XApi\\LrsBundle\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"spec\\XApi\\LrsBundle\\": "spec/"
}
},
"extra": {
"branch-alias": {
"dev-master": "0.1.x-dev"
}
}
}

View File

@@ -0,0 +1,209 @@
<?php
/*
* This file is part of the xAPI package.
*
* (c) Christian Flothmann <christian.flothmann@xabbuh.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace XApi\LrsBundle\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\ParameterBag;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Xabbuh\XApi\Common\Exception\NotFoundException;
use Xabbuh\XApi\Model\IRL;
use Xabbuh\XApi\Model\Statement;
use Xabbuh\XApi\Model\StatementId;
use Xabbuh\XApi\Model\StatementResult;
use Xabbuh\XApi\Serializer\StatementResultSerializerInterface;
use Xabbuh\XApi\Serializer\StatementSerializerInterface;
use XApi\LrsBundle\Model\StatementsFilterFactory;
use XApi\LrsBundle\Response\AttachmentResponse;
use XApi\LrsBundle\Response\MultipartResponse;
use XApi\Repository\Api\StatementRepositoryInterface;
/**
* @author Jérôme Parmentier <jerome.parmentier@acensi.fr>
*/
class StatementGetController
{
protected static $getParameters = [
'statementId' => true,
'voidedStatementId' => true,
'agent' => true,
'verb' => true,
'activity' => true,
'registration' => true,
'related_activities' => true,
'related_agents' => true,
'since' => true,
'until' => true,
'limit' => true,
'format' => true,
'attachments' => true,
'ascending' => true,
'cursor' => true,
];
protected $repository;
protected $statementSerializer;
protected $statementResultSerializer;
protected $statementsFilterFactory;
public function __construct(StatementRepositoryInterface $repository, StatementSerializerInterface $statementSerializer, StatementResultSerializerInterface $statementResultSerializer, StatementsFilterFactory $statementsFilterFactory)
{
$this->repository = $repository;
$this->statementSerializer = $statementSerializer;
$this->statementResultSerializer = $statementResultSerializer;
$this->statementsFilterFactory = $statementsFilterFactory;
}
/**
* @throws BadRequestHttpException if the query parameters does not comply with xAPI specification
*
* @return Response
*/
public function getStatement(Request $request)
{
$query = new ParameterBag(\array_intersect_key($request->query->all(), self::$getParameters));
$this->validate($query);
$includeAttachments = $query->filter('attachments', false, FILTER_VALIDATE_BOOLEAN);
try {
if (($statementId = $query->get('statementId')) !== null) {
$statement = $this->repository->findStatementById(StatementId::fromString($statementId));
$response = $this->buildSingleStatementResponse($statement, $includeAttachments);
} elseif (($voidedStatementId = $query->get('voidedStatementId')) !== null) {
$statement = $this->repository->findVoidedStatementById(StatementId::fromString($voidedStatementId));
$response = $this->buildSingleStatementResponse($statement, $includeAttachments);
} else {
$statements = $this->repository->findStatementsBy($this->statementsFilterFactory->createFromParameterBag($query));
$response = $this->buildMultiStatementsResponse($statements, $query, $includeAttachments);
}
} catch (NotFoundException $e) {
$response = $this->buildMultiStatementsResponse([], $query)
->setStatusCode(Response::HTTP_NOT_FOUND)
->setContent('');
} catch (\Exception $exception) {
$response = Response::create('', Response::HTTP_BAD_REQUEST);
}
$now = new \DateTime();
$response->headers->set('X-Experience-API-Consistent-Through', $now->format(\DateTime::ATOM));
$response->headers->set('Content-Type', 'application/json');
return $response;
}
/**
* @param bool $includeAttachments true to include the attachments in the response, false otherwise
*
* @return JsonResponse|MultipartResponse
*/
protected function buildSingleStatementResponse(Statement $statement, $includeAttachments = false)
{
$json = $this->statementSerializer->serializeStatement($statement);
$response = new Response($json, 200);
if ($includeAttachments) {
$response = $this->buildMultipartResponse($response, [$statement]);
}
$response->setLastModified($statement->getStored());
return $response;
}
/**
* @param Statement[] $statements
* @param bool $includeAttachments true to include the attachments in the response, false otherwise
*
* @return JsonResponse|MultipartResponse
*/
protected function buildMultiStatementsResponse(array $statements, ParameterBag $query, $includeAttachments = false)
{
$moreUrlPath = $statements ? $this->generateMoreIrl($query) : null;
$json = $this->statementResultSerializer->serializeStatementResult(
new StatementResult($statements, $moreUrlPath)
);
$response = new Response($json, 200);
if ($includeAttachments) {
$response = $this->buildMultipartResponse($response, $statements);
}
return $response;
}
/**
* @param Statement[] $statements
*
* @return MultipartResponse
*/
protected function buildMultipartResponse(JsonResponse $statementResponse, array $statements)
{
$attachmentsParts = [];
foreach ($statements as $statement) {
foreach ((array) $statement->getAttachments() as $attachment) {
$attachmentsParts[] = new AttachmentResponse($attachment);
}
}
return new MultipartResponse($statementResponse, $attachmentsParts);
}
/**
* Validate the parameters.
*
* @throws BadRequestHttpException if the parameters does not comply with the xAPI specification
*/
protected function validate(ParameterBag $query)
{
$hasStatementId = $query->has('statementId');
$hasVoidedStatementId = $query->has('voidedStatementId');
if ($hasStatementId && $hasVoidedStatementId) {
throw new BadRequestHttpException('Request must not have both statementId and voidedStatementId parameters at the same time.');
}
$hasAttachments = $query->has('attachments');
$hasFormat = $query->has('format');
$queryCount = $query->count();
if (($hasStatementId || $hasVoidedStatementId) && $hasAttachments && $hasFormat && $queryCount > 3) {
throw new BadRequestHttpException('Request must not contain statementId or voidedStatementId parameters, and also any other parameter besides "attachments" or "format".');
}
if (($hasStatementId || $hasVoidedStatementId) && ($hasAttachments || $hasFormat) && $queryCount > 2) {
throw new BadRequestHttpException('Request must not contain statementId or voidedStatementId parameters, and also any other parameter besides "attachments" or "format".');
}
if (($hasStatementId || $hasVoidedStatementId) && $queryCount > 1) {
throw new BadRequestHttpException('Request must not contain statementId or voidedStatementId parameters, and also any other parameter besides "attachments" or "format".');
}
}
protected function generateMoreIrl(ParameterBag $query): IRL
{
$params = $query->all();
$params['cursor'] = empty($params['cursor']) ? 1 : $params['cursor'] + 1;
return IRL::fromString(
'/plugin/xapi/lrs.php/statements?'.http_build_query($params)
);
}
}

View File

@@ -0,0 +1,27 @@
<?php
/*
* This file is part of the xAPI package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace XApi\LrsBundle\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
class StatementHeadController extends StatementGetController
{
/**
* @throws BadRequestHttpException if the query parameters does not comply with xAPI specification
*
* @return Response
*/
public function getStatement(Request $request)
{
return parent::getStatement($request)->setContent('');
}
}

View File

@@ -0,0 +1,67 @@
<?php
/*
* This file is part of the xAPI package.
*
* (c) Christian Flothmann <christian.flothmann@xabbuh.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace XApi\LrsBundle\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
use Xabbuh\XApi\Common\Exception\NotFoundException;
use Xabbuh\XApi\Model\Statement;
use XApi\Repository\Api\StatementRepositoryInterface;
/**
* @author Jérôme Parmentier <jerome.parmentier@acensi.fr>
*/
final class StatementPostController
{
/**
* @var StatementRepositoryInterface
*/
private $repository;
public function __construct(StatementRepositoryInterface $repository)
{
$this->repository = $repository;
}
public function postStatements(Request $request, array $statements): JsonResponse
{
$statementsToStore = [];
/** @var Statement $statement */
foreach ($statements as $statement) {
if (null === $statementId = $statement->getId()) {
$statementsToStore[] = $statement;
continue;
}
try {
$existingStatement = $this->repository->findStatementById($statement->getId());
if (!$existingStatement->equals($statement)) {
throw new ConflictHttpException('The new statement is not equal to an existing statement with the same id.');
}
} catch (NotFoundException $e) {
$statementsToStore[] = $statement;
}
}
$uuids = [];
foreach ($statementsToStore as $statement) {
$uuids[] = $this->repository->storeStatement($statement, true)->getValue();
}
return new JsonResponse($uuids);
}
}

View File

@@ -0,0 +1,65 @@
<?php
/*
* This file is part of the xAPI package.
*
* (c) Christian Flothmann <christian.flothmann@xabbuh.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace XApi\LrsBundle\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
use Xabbuh\XApi\Common\Exception\NotFoundException;
use Xabbuh\XApi\Model\Statement;
use Xabbuh\XApi\Model\StatementId;
use XApi\Repository\Api\StatementRepositoryInterface;
/**
* @author Christian Flothmann <christian.flothmann@xabbuh.de>
*/
final class StatementPutController
{
private $repository;
public function __construct(StatementRepositoryInterface $repository)
{
$this->repository = $repository;
}
public function putStatement(Request $request, Statement $statement): Response
{
if (null === $statementId = $request->query->get('statementId')) {
throw new BadRequestHttpException('Required statementId parameter is missing.');
}
try {
$id = StatementId::fromString($statementId);
} catch (\InvalidArgumentException $e) {
throw new BadRequestHttpException(sprintf('Parameter statementId ("%s") is not a valid UUID.', $statementId), $e);
}
if (null !== $statement->getId() && !$id->equals($statement->getId())) {
throw new ConflictHttpException(sprintf('Id parameter ("%s") and statement id ("%s") do not match.', $id->getValue(), $statement->getId()->getValue()));
}
try {
$existingStatement = $this->repository->findStatementById($id);
if (!$existingStatement->equals($statement)) {
throw new ConflictHttpException('The new statement is not equal to an existing statement with the same id.');
}
} catch (NotFoundException $e) {
$statement = $statement->withId($id);
$this->repository->storeStatement($statement, true);
}
return new Response('', 204);
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace XApi\LrsBundle\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
/**
* @author Christian Flothmann <christian.flothmann@xabbuh.de>
*/
final class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$treeBuilder
->root('xapi_lrs')
->beforeNormalization()
->ifTrue(function ($v) { return isset($v['type']) && in_array($v['type'], ['mongodb', 'orm']) && !isset($v['object_manager_service']); })
->thenInvalid('You need to configure the object manager service when the repository type is "mongodb" or orm".')
->end()
->children()
->enumNode('type')
->isRequired()
->values(['in_memory', 'mongodb', 'orm'])
->end()
->scalarNode('object_manager_service')->end()
->end()
->end()
;
return $treeBuilder;
}
}

View File

@@ -0,0 +1,60 @@
<?php
/*
* This file is part of the xAPI package.
*
* (c) Christian Flothmann <christian.flothmann@xabbuh.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace XApi\LrsBundle\DependencyInjection;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
/**
* @author Christian Flothmann <christian.flothmann@xabbuh.de>
*/
final class XApiLrsExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
$loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('controller.xml');
$loader->load('event_listener.xml');
$loader->load('factory.xml');
$loader->load('serializer.xml');
switch ($config['type']) {
case 'in_memory':
break;
case 'mongodb':
$loader->load('doctrine.xml');
$loader->load('mongodb.xml');
$container->setAlias('xapi_lrs.doctrine.object_manager', $config['object_manager_service']);
$container->setAlias('xapi_lrs.repository.statement', 'xapi_lrs.repository.statement.doctrine');
break;
case 'orm':
$loader->load('doctrine.xml');
$loader->load('orm.xml');
$container->setAlias('xapi_lrs.doctrine.object_manager', $config['object_manager_service']);
$container->setAlias('xapi_lrs.repository.statement', 'xapi_lrs.repository.statement.doctrine');
break;
}
}
public function getAlias()
{
return 'xapi_lrs';
}
}

View File

@@ -0,0 +1,73 @@
<?php
/*
* This file is part of the xAPI package.
*
* (c) Christian Flothmann <christian.flothmann@xabbuh.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace XApi\LrsBundle\EventListener;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
/**
* @author Jérôme Parmentier <jerome.parmentier@acensi.fr>
*/
class AlternateRequestSyntaxListener
{
public function onKernelRequest(GetResponseEvent $event)
{
if (!$event->isMasterRequest()) {
return;
}
$request = $event->getRequest();
if (!$request->attributes->has('xapi_lrs.route')) {
return;
}
if ('POST' !== $request->getMethod()) {
return;
}
if (null === $method = $request->query->get('method')) {
return;
}
if ($request->query->count() > 1) {
throw new BadRequestHttpException('Including other query parameters than "method" is not allowed. You have to send them as POST parameters inside the request body.');
}
$request->setMethod($method);
$request->query->remove('method');
if (null !== $content = $request->request->get('content')) {
$request->request->remove('content');
$request->initialize(
$request->query->all(),
$request->request->all(),
$request->attributes->all(),
$request->cookies->all(),
$request->files->all(),
$request->server->all(),
$content
);
}
foreach ($request->request as $key => $value) {
if (in_array($key, ['Authorization', 'X-Experience-API-Version', 'Content-Type', 'Content-Length', 'If-Match', 'If-None-Match'], true)) {
$request->headers->set($key, $value);
} else {
$request->query->set($key, $value);
}
$request->request->remove($key);
}
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace XApi\LrsBundle\EventListener;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
/**
* Converts Experience API specific domain exceptions into proper HTTP responses.
*
* @author Christian Flothmann <christian.flothmann@xabbuh.de>
*/
class ExceptionListener
{
public function onKernelException(GetResponseForExceptionEvent $event)
{
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace XApi\LrsBundle\EventListener;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Serializer\Exception\ExceptionInterface as BaseSerializerException;
use Xabbuh\XApi\Serializer\StatementSerializerInterface;
/**
* @author Christian Flothmann <christian.flothmann@xabbuh.de>
*/
class SerializerListener
{
private $statementSerializer;
public function __construct(StatementSerializerInterface $statementSerializer)
{
$this->statementSerializer = $statementSerializer;
}
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
if (!$request->attributes->has('xapi_lrs.route')) {
return;
}
try {
switch ($request->attributes->get('xapi_serializer')) {
case 'statement':
$request->attributes->set('statement', $this->statementSerializer->deserializeStatement($request->getContent()));
break;
}
} catch (BaseSerializerException $e) {
throw new BadRequestHttpException(sprintf('The content of the request cannot be deserialized into a valid xAPI %s.', $request->attributes->get('xapi_serializer')), $e);
}
}
}

View File

@@ -0,0 +1,66 @@
<?php
/*
* This file is part of the xAPI package.
*
* (c) Christian Flothmann <christian.flothmann@xabbuh.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace XApi\LrsBundle\EventListener;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
/**
* @author Jérôme Parmentier <jerome.parmentier@acensi.fr>
*/
class VersionListener
{
public function onKernelRequest(GetResponseEvent $event)
{
if (!$event->isMasterRequest()) {
return;
}
$request = $event->getRequest();
if (!$request->attributes->has('xapi_lrs.route')) {
return;
}
if (null === $version = $request->headers->get('X-Experience-API-Version')) {
throw new BadRequestHttpException('Missing required "X-Experience-API-Version" header.');
}
if (preg_match('/^1\.0(?:\.\d+)?$/', $version)) {
if ('1.0' === $version) {
$request->headers->set('X-Experience-API-Version', '1.0.0');
}
return;
}
throw new BadRequestHttpException(sprintf('xAPI version "%s" is not supported.', $version));
}
public function onKernelResponse(FilterResponseEvent $event)
{
if (!$event->isMasterRequest()) {
return;
}
if (!$event->getRequest()->attributes->has('xapi_lrs.route')) {
return;
}
$headers = $event->getResponse()->headers;
if (!$headers->has('X-Experience-API-Version')) {
$headers->set('X-Experience-API-Version', '1.0.3');
}
}
}

View File

@@ -0,0 +1,86 @@
<?php
/*
* This file is part of the xAPI package.
*
* (c) Christian Flothmann <christian.flothmann@xabbuh.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace XApi\LrsBundle\Model;
use Symfony\Component\HttpFoundation\ParameterBag;
use Xabbuh\XApi\Model\Activity;
use Xabbuh\XApi\Model\IRI;
use Xabbuh\XApi\Model\StatementsFilter;
use Xabbuh\XApi\Model\Verb;
use Xabbuh\XApi\Serializer\ActorSerializerInterface;
/**
* @author Jérôme Parmentier <jerome.parmentier@acensi.fr>
*/
class StatementsFilterFactory
{
private $actorSerializer;
public function __construct(ActorSerializerInterface $actorSerializer)
{
$this->actorSerializer = $actorSerializer;
}
/**
* @return StatementsFilter
*/
public function createFromParameterBag(ParameterBag $parameters)
{
$filter = new StatementsFilter();
if (($actor = $parameters->get('agent')) !== null) {
$filter->byActor($this->actorSerializer->deserializeActor($actor));
}
if (($verbId = $parameters->get('verb')) !== null) {
$filter->byVerb(new Verb(IRI::fromString($verbId)));
}
if (($activityId = $parameters->get('activity')) !== null) {
$filter->byActivity(new Activity(IRI::fromString($activityId)));
}
if (($registration = $parameters->get('registration')) !== null) {
$filter->byRegistration($registration);
}
if ($parameters->filter('related_activities', false, FILTER_VALIDATE_BOOLEAN)) {
$filter->enableRelatedActivityFilter();
} else {
$filter->disableRelatedActivityFilter();
}
if ($parameters->filter('related_agents', false, FILTER_VALIDATE_BOOLEAN)) {
$filter->enableRelatedAgentFilter();
} else {
$filter->disableRelatedAgentFilter();
}
if (($since = $parameters->get('since')) !== null) {
$filter->since(\DateTime::createFromFormat(\DateTime::ATOM, $since));
}
if (($until = $parameters->get('until')) !== null) {
$filter->until(\DateTime::createFromFormat(\DateTime::ATOM, $until));
}
if ($parameters->filter('ascending', false, FILTER_VALIDATE_BOOLEAN)) {
$filter->ascending();
} else {
$filter->descending();
}
$filter->limit($parameters->getInt('limit'));
return $filter;
}
}

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services
http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="xapi_lrs.controller.statement.get" class="XApi\LrsBundle\Controller\StatementGetController">
<argument type="service" id="xapi_lrs.repository.statement"/>
<argument type="service" id="xapi_lrs.statement.serializer"/>
<argument type="service" id="xapi_lrs.statement_result.serializer"/>
<argument type="service" id="xapi_lrs.factory.statements_filter"/>
</service>
<service id="xapi_lrs.controller.statement.post" class="XApi\LrsBundle\Controller\StatementPostController"/>
<service id="xapi_lrs.controller.statement.put" class="XApi\LrsBundle\Controller\StatementPutController">
<argument type="service" id="xapi_lrs.repository.statement"/>
</service>
</services>
</container>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services
http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="xapi_lrs.repository.statement.doctrine" class="XApi\Repository\Doctrine\Repository\StatementRepository" public="false">
<argument type="service" id="xapi_lrs.repository.mapped_statement" />
</service>
</services>
</container>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services
http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="xapi_lrs.event_listener.alternate_request_syntax" class="XApi\LrsBundle\EventListener\AlternateRequestSyntaxListener">
<tag name="kernel.event_listener" event="kernel.request" />
</service>
<service id="xapi_lrs.event_listener.exception" class="XApi\LrsBundle\EventListener\ExceptionListener">
</service>
<service id="xapi_lrs.event_listener.serializer" class="XApi\LrsBundle\EventListener\SerializerListener">
<argument type="service" id="xapi_lrs.statement.serializer" />
<tag name="kernel.event_listener" event="kernel.request" />
</service>
<service id="xapi_lrs.event_listener.version" class="XApi\LrsBundle\EventListener\VersionListener">
<tag name="kernel.event_listener" event="kernel.request" />
<tag name="kernel.event_listener" event="kernel.response" />
</service>
</services>
</container>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services
http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="xapi_lrs.factory.statements_filter" class="XApi\LrsBundle\Model\StatementsFilterFactory">
<argument type="service" id="xapi_lrs.actor.serializer"/>
</service>
</services>
</container>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services
http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="xapi_lrs.doctrine.class_metadata" class="Doctrine\ORM\Mapping\ClassMetadata" public="false">
<argument>XApi\Repository\Api\Mapping\MappedStatement</argument>
<factory service="xapi_lrs.doctrine.object_manager" method="getClassMetadata" />
</service>
<service id="xapi_lrs.repository.mapped_statement" class="XApi\Repository\ORM\MappedStatementRepository" public="false">
<argument type="service" id="xapi_lrs.doctrine.object_manager" />
<argument type="service" id="xapi_lrs.doctrine.class_metadata" />
</service>
</services>
</container>

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing
http://symfony.com/schema/routing/routing-1.0.xsd">
<route id="xapi_lrs.statement.put" path="/statements" methods="PUT">
<default key="_controller">xapi_lrs.controller.statement.put:putStatement</default>
<default key="xapi_serializer">statement</default>
<default key="xapi_lrs.route">
<bool>true</bool>
</default>
</route>
<route id="xapi_lrs.statement.post" path="/statements" methods="POST">
<default key="_controller">xapi_lrs.controller.statement.post:postStatement</default>
<default key="xapi_serializer">statement</default>
<default key="xapi_lrs.route">
<bool>true</bool>
</default>
</route>
<route id="xapi_lrs.statement.get" path="/statements" methods="GET">
<default key="_controller">xapi_lrs.controller.statement.get:getStatement</default>
<default key="xapi_lrs.route">
<bool>true</bool>
</default>
</route>
</routes>

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services
http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="xapi_lrs.statement.serializer" class="Xabbuh\XApi\Serializer\StatementSerializerInterface" public="false">
<factory service="xapi_lrs.serializer.factory" method="createStatementSerializer"/>
</service>
<service id="xapi_lrs.statement_result.serializer" class="Xabbuh\XApi\Serializer\StatementResultSerializerInterface" public="false">
<factory service="xapi_lrs.serializer.factory" method="createStatementResultSerializer"/>
</service>
<service id="xapi_lrs.actor.serializer" class="Xabbuh\XApi\Serializer\ActorSerializerInterface" public="false">
<factory service="xapi_lrs.serializer.factory" method="createActorSerializer"/>
</service>
<service id="xapi_lrs.document_data.serializer" class="Xabbuh\XApi\Serializer\DocumentDataSerializerInterface" public="false">
<factory service="xapi_lrs.serializer.factory" method="createDocumentDataSerializer"/>
</service>
<service id="xapi_lrs.serializer_factory" class="Xabbuh\XApi\Serializer\Symfony\SerializerFactory" public="false">
<argument type="service" id="xapi_lrs.serializer"/>
</service>
<service id="xapi_lrs.serializer" class="Symfony\Component\Serializer\SerializerInterface" public="false">
<factory class="Xabbuh\XApi\Serializer\Symfony\Serializer" method="createSerializer"/>
</service>
</services>
</container>

View File

@@ -0,0 +1,76 @@
<?php
/*
* This file is part of the xAPI package.
*
* (c) Christian Flothmann <christian.flothmann@xabbuh.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace XApi\LrsBundle\Response;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Xabbuh\XApi\Model\Attachment;
/**
* @author Jérôme Parmentier <jerome.parmentier@acensi.fr>
*/
class AttachmentResponse extends Response
{
protected $attachment;
public function __construct(Attachment $attachment)
{
parent::__construct(null);
$this->attachment = $attachment;
}
/**
* {@inheritdoc}
*/
public function prepare(Request $request)
{
if (!$this->headers->has('Content-Type')) {
$this->headers->set('Content-Type', $this->attachment->getContentType());
}
$this->headers->set('Content-Transfer-Encoding', 'binary');
$this->headers->set('X-Experience-API-Hash', $this->attachment->getSha2());
}
/**
* {@inheritdoc}
*
* @throws \LogicException
*/
public function sendContent()
{
throw new \LogicException('An AttachmentResponse is only meant to be part of a multipart Response.');
}
/**
* {@inheritdoc}
*
* @throws \LogicException when the content is not null
*/
public function setContent($content)
{
if (null !== $content) {
throw new \LogicException('The content cannot be set on an AttachmentResponse instance.');
}
}
/**
* {@inheritdoc}
*
* @return string|null
*/
public function getContent()
{
return $this->attachment->getContent();
}
}

View File

@@ -0,0 +1,135 @@
<?php
/*
* This file is part of the xAPI package.
*
* (c) Christian Flothmann <christian.flothmann@xabbuh.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace XApi\LrsBundle\Response;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* @author Jérôme Parmentier <jerome.parmentier@acensi.fr>
*/
class MultipartResponse extends Response
{
protected $subtype;
protected $boundary;
protected $statementPart;
/**
* @var Response[]
*/
protected $parts;
/**
* @param AttachmentResponse[] $attachmentsParts
* @param int $status
* @param string|null $subtype
*/
public function __construct(JsonResponse $statementPart, array $attachmentsParts = [], $status = 200, array $headers = [], $subtype = null)
{
parent::__construct(null, $status, $headers);
if (null === $subtype) {
$subtype = 'mixed';
}
$this->subtype = $subtype;
$this->boundary = uniqid('', true);
$this->statementPart = $statementPart;
$this->setAttachmentsParts($attachmentsParts);
}
/**
* @return $this
*/
public function addAttachmentPart(AttachmentResponse $part)
{
if ($part->getContent() !== null) {
$this->parts[] = $part;
}
return $this;
}
/**
* @param AttachmentResponse[] $attachmentsParts
*
* @return $this
*/
public function setAttachmentsParts(array $attachmentsParts)
{
$this->parts = [$this->statementPart];
foreach ($attachmentsParts as $part) {
$this->addAttachmentPart($part);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function prepare(Request $request)
{
foreach ($this->parts as $part) {
$part->prepare($request);
}
$this->headers->set('Content-Type', sprintf('multipart/%s; boundary="%s"', $this->subtype, $this->boundary));
$this->headers->set('Transfer-Encoding', 'chunked');
return parent::prepare($request);
}
/**
* {@inheritdoc}
*/
public function sendContent()
{
$content = '';
foreach ($this->parts as $part) {
$content .= sprintf('--%s', $this->boundary)."\r\n";
$content .= $part->headers."\r\n";
$content .= $part->getContent();
$content .= "\r\n";
}
$content .= sprintf('--%s--', $this->boundary)."\r\n";
echo $content;
return $this;
}
/**
* {@inheritdoc}
*
* @throws \LogicException when the content is not null
*/
public function setContent($content)
{
if (null !== $content) {
throw new \LogicException('The content cannot be set on a MultipartResponse instance.');
}
}
/**
* {@inheritdoc}
*
* @return false
*/
public function getContent()
{
return false;
}
}

View File

@@ -0,0 +1,26 @@
<?php
/*
* This file is part of the xAPI package.
*
* (c) Christian Flothmann <christian.flothmann@xabbuh.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace XApi\LrsBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use XApi\LrsBundle\DependencyInjection\XApiLrsExtension;
/**
* @author Christian Flothmann <christian.flothmann@xabbuh.de>
*/
class XApiLrsBundle extends Bundle
{
public function getContainerExtension()
{
return new XApiLrsExtension();
}
}

View File

@@ -0,0 +1,19 @@
Copyright (c) 2016-2017 Christian Flothmann
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,41 @@
{
"name": "php-xapi/repository-doctrine-orm",
"description": "Doctrine based ORM implementations of an Experience API (xAPI) repository",
"keywords": ["xAPI", "Tin Can API", "Experience API", "storage", "database", "repository", "entity", "Doctrine", "ORM"],
"homepage": "https://github.com/php-xapi/repository-orm/",
"license": "MIT",
"authors": [
{
"name": "Christian Flothmann",
"homepage": "https://github.com/xabbuh"
}
],
"require": {
"php": "^5.6 || ^7.0",
"doctrine/orm": "^2.3",
"php-xapi/repository-api": "^0.4",
"php-xapi/repository-doctrine": "^0.4"
},
"require-dev": {
"symfony/phpunit-bridge": "^3.4 || ^4.0"
},
"provide": {
"php-xapi/repository-implementation": "0.3"
},
"autoload": {
"psr-4": {
"XApi\\Repository\\ORM\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"XApi\\Repository\\ORM\\Tests\\": "tests/"
}
},
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-master": "0.1.x-dev"
}
}
}

View File

@@ -0,0 +1,19 @@
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<entity name="XApi\Repository\Doctrine\Mapping\Actor" table="xapi_actor">
<id name="identifier" type="integer">
<generator strategy="AUTO" />
</id>
<field name="type" type="string" nullable="true" />
<field name="mbox" type="string" nullable="true" />
<field name="mboxSha1Sum" type="string" nullable="true" />
<field name="openId" type="string" nullable="true" />
<field name="accountName" type="string" nullable="true" />
<field name="accountHomePage" type="string" nullable="true" />
<field name="name" type="string" nullable="true" />
</entity>
</doctrine-mapping>

View File

@@ -0,0 +1,23 @@
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<entity name="XApi\Repository\Doctrine\Mapping\Attachment" table="xapi_attachment">
<id name="identifier" type="integer">
<generator strategy="AUTO" />
</id>
<field name="usageType" type="string" />
<field name="contentType" type="string" />
<field name="length" type="integer" />
<field name="sha2" type="string" />
<field name="display" type="json_array" />
<field name="hasDescription" type="boolean" />
<field name="description" type="json_array" nullable="true" />
<field name="fileUrl" type="string" nullable="true" />
<field name="content" type="text" nullable="true" />
<many-to-one field="statement" target-entity="XApi\Repository\Doctrine\Mapping\Statement" inversed-by="attachments" />
</entity>
</doctrine-mapping>

View File

@@ -0,0 +1,59 @@
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<entity name="XApi\Repository\Doctrine\Mapping\Context" table="xapi_context">
<id name="identifier" type="integer">
<generator strategy="AUTO" />
</id>
<field name="registration" type="string" nullable="true" />
<field name="hasContextActivities" type="boolean" nullable="true" />
<field name="revision" type="string" nullable="true" />
<field name="platform" type="string" nullable="true" />
<field name="language" type="string" nullable="true" />
<field name="statement" type="string" nullable="true" />
<one-to-one field="instructor" target-entity="XApi\Repository\Doctrine\Mapping\StatementObject">
<cascade>
<cascade-all />
</cascade>
<join-column referenced-column-name="identifier" />
</one-to-one>
<one-to-one field="team" target-entity="XApi\Repository\Doctrine\Mapping\StatementObject">
<cascade>
<cascade-all />
</cascade>
<join-column referenced-column-name="identifier" />
</one-to-one>
<one-to-one field="extensions" target-entity="XApi\Repository\Doctrine\Mapping\Extensions">
<cascade>
<cascade-all />
</cascade>
<join-column referenced-column-name="identifier" />
</one-to-one>
<!-- context activities -->
<one-to-many field="parentActivities" target-entity="XApi\Repository\Doctrine\Mapping\StatementObject" mapped-by="parentContext">
<cascade>
<cascade-all />
</cascade>
</one-to-many>
<one-to-many field="groupingActivities" target-entity="XApi\Repository\Doctrine\Mapping\StatementObject" mapped-by="groupingContext">
<cascade>
<cascade-all />
</cascade>
</one-to-many>
<one-to-many field="categoryActivities" target-entity="XApi\Repository\Doctrine\Mapping\StatementObject" mapped-by="categoryContext">
<cascade>
<cascade-all />
</cascade>
</one-to-many>
<one-to-many field="otherActivities" target-entity="XApi\Repository\Doctrine\Mapping\StatementObject" mapped-by="otherContext">
<cascade>
<cascade-all />
</cascade>
</one-to-many>
</entity>
</doctrine-mapping>

View File

@@ -0,0 +1,13 @@
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<entity name="XApi\Repository\Doctrine\Mapping\Extensions" table="xapi_extensions">
<id name="identifier" type="integer">
<generator strategy="AUTO" />
</id>
<field name="extensions" type="json_array" />
</entity>
</doctrine-mapping>

View File

@@ -0,0 +1,28 @@
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<entity name="XApi\Repository\Doctrine\Mapping\Result" table="xapi_result">
<id name="identifier" type="integer">
<generator strategy="AUTO" />
</id>
<field name="hasScore" type="boolean" />
<field name="scaled" type="float" nullable="true" />
<field name="raw" type="float" nullable="true" />
<field name="min" type="float" nullable="true" />
<field name="max" type="float" nullable="true" />
<field name="success" type="boolean" nullable="true" />
<field name="completion" type="boolean" nullable="true" />
<field name="response" type="string" nullable="true" />
<field name="duration" type="string" nullable="true" />
<one-to-one field="extensions" target-entity="XApi\Repository\Doctrine\Mapping\Extensions">
<cascade>
<cascade-all />
</cascade>
<join-column referenced-column-name="identifier" />
</one-to-one>
</entity>
</doctrine-mapping>

View File

@@ -0,0 +1,62 @@
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<entity name="XApi\Repository\Doctrine\Mapping\Statement"
repository-class="XApi\Repository\ORM\StatementRepository"
table="xapi_statement">
<id name="id" type="string">
<generator strategy="NONE" />
</id>
<field name="created" type="bigint" nullable="true" />
<field name="stored" type="bigint" nullable="true" />
<field name="hasAttachments" type="boolean" />
<one-to-one field="actor" target-entity="XApi\Repository\Doctrine\Mapping\StatementObject">
<cascade>
<cascade-all />
</cascade>
<join-column referenced-column-name="identifier" />
</one-to-one>
<one-to-one field="verb" target-entity="XApi\Repository\Doctrine\Mapping\Verb">
<cascade>
<cascade-all />
</cascade>
<join-column referenced-column-name="identifier" />
</one-to-one>
<one-to-one field="object" target-entity="XApi\Repository\Doctrine\Mapping\StatementObject">
<cascade>
<cascade-all />
</cascade>
<join-column referenced-column-name="identifier" />
</one-to-one>
<one-to-one field="result" target-entity="XApi\Repository\Doctrine\Mapping\Result">
<cascade>
<cascade-all />
</cascade>
<join-column referenced-column-name="identifier" />
</one-to-one>
<one-to-one field="authority" target-entity="XApi\Repository\Doctrine\Mapping\StatementObject">
<cascade>
<cascade-all />
</cascade>
<join-column referenced-column-name="identifier" />
</one-to-one>
<one-to-one field="context" target-entity="XApi\Repository\Doctrine\Mapping\Context">
<cascade>
<cascade-all />
</cascade>
<join-column referenced-column-name="identifier" />
</one-to-one>
<!-- attachments -->
<one-to-many field="attachments" target-entity="XApi\Repository\Doctrine\Mapping\Attachment" mapped-by="statement">
<cascade>
<cascade-all />
</cascade>
</one-to-many>
</entity>
</doctrine-mapping>

View File

@@ -0,0 +1,83 @@
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<entity name="XApi\Repository\Doctrine\Mapping\StatementObject" table="xapi_object">
<id name="identifier" type="integer">
<generator strategy="AUTO" />
</id>
<!-- discriminator column -->
<field name="type" type="string" nullable="true" />
<!-- activity -->
<field name="activityId" type="string" nullable="true" />
<field name="hasActivityDefinition" type="boolean" nullable="true" />
<field name="hasActivityName" type="boolean" nullable="true" />
<field name="activityName" type="json_array" nullable="true" />
<field name="hasActivityDescription" type="boolean" nullable="true" />
<field name="activityDescription" type="json_array" nullable="true" />
<field name="activityType" type="string" nullable="true" />
<field name="activityMoreInfo" type="string" nullable="true" />
<!-- actor -->
<field name="mbox" type="string" nullable="true" />
<field name="mboxSha1Sum" type="string" nullable="true" />
<field name="openId" type="string" nullable="true" />
<field name="accountName" type="string" nullable="true" />
<field name="accountHomePage" type="string" nullable="true" />
<field name="name" type="string" nullable="true" />
<!-- statement reference -->
<field name="referencedStatementId" type="string" nullable="true" />
<!-- sub statement -->
<one-to-one field="actor" target-entity="XApi\Repository\Doctrine\Mapping\StatementObject">
<cascade>
<cascade-all />
</cascade>
<join-column referenced-column-name="identifier" />
</one-to-one>
<one-to-one field="verb" target-entity="XApi\Repository\Doctrine\Mapping\Verb">
<cascade>
<cascade-all />
</cascade>
<join-column referenced-column-name="identifier" />
</one-to-one>
<one-to-one field="object" target-entity="XApi\Repository\Doctrine\Mapping\StatementObject">
<cascade>
<cascade-all />
</cascade>
<join-column referenced-column-name="identifier" />
</one-to-one>
<!-- activity extensions -->
<one-to-one field="activityExtensions" target-entity="XApi\Repository\Doctrine\Mapping\Extensions">
<cascade>
<cascade-all />
</cascade>
<join-column referenced-column-name="identifier" />
</one-to-one>
<!-- group members -->
<one-to-many target-entity="XApi\Repository\Doctrine\Mapping\StatementObject" mapped-by="group" field="members" />
<many-to-one target-entity="XApi\Repository\Doctrine\Mapping\StatementObject" field="group" inversed-by="members">
<join-column referenced-column-name="identifier" />
</many-to-one>
<!-- context activities -->
<many-to-one target-entity="XApi\Repository\Doctrine\Mapping\Context" field="parentContext" inversed-by="parentActivities">
<join-column referenced-column-name="identifier" />
</many-to-one>
<many-to-one target-entity="XApi\Repository\Doctrine\Mapping\Context" field="groupingContext" inversed-by="groupingActivities">
<join-column referenced-column-name="identifier" />
</many-to-one>
<many-to-one target-entity="XApi\Repository\Doctrine\Mapping\Context" field="categoryContext" inversed-by="categoryActivities">
<join-column referenced-column-name="identifier" />
</many-to-one>
<many-to-one target-entity="XApi\Repository\Doctrine\Mapping\Context" field="otherContext" inversed-by="otherActivities">
<join-column referenced-column-name="identifier" />
</many-to-one>
</entity>
</doctrine-mapping>

View File

@@ -0,0 +1,14 @@
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<entity name="XApi\Repository\Doctrine\Mapping\Verb" table="xapi_verb">
<id name="identifier" type="integer">
<generator strategy="AUTO" />
</id>
<field name="id" type="string" />
<field name="display" type="json_array" />
</entity>
</doctrine-mapping>

View File

@@ -0,0 +1,67 @@
<?php
/*
* This file is part of the xAPI package.
*
* (c) Christian Flothmann <christian.flothmann@xabbuh.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace XApi\Repository\ORM;
use Doctrine\ORM\EntityRepository;
use XApi\Repository\Doctrine\Mapping\Context;
use XApi\Repository\Doctrine\Mapping\Statement;
use XApi\Repository\Doctrine\Repository\Mapping\StatementRepository as BaseStatementRepository;
/**
* @author Christian Flothmann <christian.flothmann@xabbuh.de>
*/
final class StatementRepository extends EntityRepository implements BaseStatementRepository
{
/**
* {@inheritdoc}
*/
public function findStatement(array $criteria)
{
return parent::findOneBy($criteria);
}
/**
* {@inheritdoc}
*/
public function findStatements(array $criteria)
{
if (!empty($criteria['registration'])) {
$contexts = $this->_em->getRepository(Context::class)->findBy([
'registration' => $criteria['registration'],
]);
$criteria['context'] = $contexts;
}
unset(
$criteria['registration'],
$criteria['related_activities'],
$criteria['related_agents'],
$criteria['ascending'],
$criteria['limit']
);
return parent::findBy($criteria, ['created' => 'ASC']);
}
/**
* {@inheritdoc}
*/
public function storeStatement(Statement $mappedStatement, $flush = true)
{
$this->_em->persist($mappedStatement);
if ($flush) {
$this->_em->flush();
}
}
}

5
plugin/xapi/plugin.php Normal file
View File

@@ -0,0 +1,5 @@
<?php
/* For licensing terms, see /license.txt */
$plugin_info = XApiPlugin::create()->get_info();

View File

@@ -0,0 +1,93 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Entity\XApi;
use Doctrine\ORM\Mapping as ORM;
/**
* Class ActivityProfile.
*
* @package Chamilo\PluginBundle\Entity\XApi
*
* @ORM\Table(name="xapi_activity_profile")
* @ORM\Entity()
*/
class ActivityProfile
{
/**
* @var int
*
* @ORM\Column(type="integer", name="id")
* @ORM\Id()
* @ORM\GeneratedValue()
*/
private $id;
/**
* @var string
*
* @ORM\Column(name="profile_id", type="string")
*/
private $profileId;
/**
* @var string
*
* @ORM\Column(name="activity_id", type="string")
*/
private $activityId;
/**
* @var array
*
* @ORM\Column(name="document_data", type="json")
*/
private $documentData;
public function getId(): int
{
return $this->id;
}
public function setId(int $id): ActivityProfile
{
$this->id = $id;
return $this;
}
public function getProfileId(): string
{
return $this->profileId;
}
public function setProfileId(string $profileId): ActivityProfile
{
$this->profileId = $profileId;
return $this;
}
public function getActivityId(): string
{
return $this->activityId;
}
public function setActivityId(string $activityId): ActivityProfile
{
$this->activityId = $activityId;
return $this;
}
public function getDocumentData(): array
{
return $this->documentData;
}
public function setDocumentData(array $documentData): ActivityProfile
{
$this->documentData = $documentData;
return $this;
}
}

View File

@@ -0,0 +1,111 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Entity\XApi;
use Doctrine\ORM\Mapping as ORM;
/**
* Class ActivityState.
*
* @package Chamilo\PluginBundle\Entity\XApi
*
* @ORM\Table(name="xapi_activity_state")
* @ORM\Entity()
*/
class ActivityState
{
/**
* @var int
*
* @ORM\Column(type="integer", name="id")
* @ORM\Id()
* @ORM\GeneratedValue()
*/
private $id;
/**
* @var string
*
* @ORM\Column(name="state_id", type="string")
*/
private $stateId;
/**
* @var string
*
* @ORM\Column(name="activity_id", type="string")
*/
private $activityId;
/**
* @var array
*
* @ORM\Column(name="agent", type="json")
*/
private $agent;
/**
* @var array
*
* @ORM\Column(name="document_data", type="json")
*/
private $documentData;
public function getId(): int
{
return $this->id;
}
public function setId(int $id): ActivityState
{
$this->id = $id;
return $this;
}
public function getStateId(): string
{
return $this->stateId;
}
public function setStateId(string $stateId): ActivityState
{
$this->stateId = $stateId;
return $this;
}
public function getActivityId(): string
{
return $this->activityId;
}
public function setActivityId(string $activityId): ActivityState
{
$this->activityId = $activityId;
return $this;
}
public function getAgent(): array
{
return $this->agent;
}
public function setAgent(array $agent): ActivityState
{
$this->agent = $agent;
return $this;
}
public function getDocumentData(): array
{
return $this->documentData;
}
public function setDocumentData(array $documentData): ActivityState
{
$this->documentData = $documentData;
return $this;
}
}

View File

@@ -0,0 +1,372 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Entity\XApi;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
/**
* Class Cmi5Item.
*
* @Gedmo\Tree(type="nested")
* @ORM\Table(name="xapi_cmi5_item")
* @ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\NestedTreeRepository")
*/
class Cmi5Item
{
/**
* @var int
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id()
* @ORM\GeneratedValue()
*/
private $id;
/**
* @var string
*
* @ORM\Column(name="identifier", type="string")
*/
private $identifier;
/**
* @var string
*
* @ORM\Column(name="type", type="string")
*/
private $type;
/**
* @var array;
*
* @ORM\Column(name="title", type="json")
*/
private $title;
/**
* @var array
*
* @ORM\Column(name="description", type="json")
*/
private $description;
/**
* @var string|null
*
* @ORM\Column(name="url", type="string", nullable=true)
*/
private $url;
/**
* @var string|null
*
* @ORM\Column(name="activity_type", type="string", nullable=true)
*/
private $activityType;
/**
* @var string|null
*
* @ORM\Column(name="launch_method", type="string", nullable=true)
*/
private $launchMethod;
/**
* @var string|null
*
* @ORM\Column(name="move_on", type="string", nullable=true)
*/
private $moveOn;
/**
* @var float|null
*
* @ORM\Column(name="mastery_score", type="float", nullable=true)
*/
private $masteryScore;
/**
* @var string|null
*
* @ORM\Column(name="launch_parameters", type="string", nullable=true)
*/
private $launchParameters;
/**
* @var string|null
*
* @ORM\Column(name="entitlement_key", type="string", nullable=true)
*/
private $entitlementKey;
/**
* @var string|null
*
* @ORM\Column(name="status", type="string", nullable=true)
*/
private $status;
/**
* @var \Chamilo\PluginBundle\Entity\XApi\ToolLaunch
*
* @ORM\ManyToOne(targetEntity="Chamilo\PluginBundle\Entity\XApi\ToolLaunch", inversedBy="items")
* @ORM\JoinColumn(name="tool_id", referencedColumnName="id", onDelete="CASCADE")
*/
private $tool;
/**
* @var int
*
* @Gedmo\TreeLeft()
* @ORM\Column(name="lft", type="integer")
*/
private $lft;
/**
* @var int
*
* @Gedmo\TreeLevel()
* @ORM\Column(name="lvl", type="integer")
*/
private $lvl;
/**
* @var int
*
* @Gedmo\TreeRight()
* @ORM\Column(name="rgt", type="integer")
*/
private $rgt;
/**
* @var \Chamilo\PluginBundle\Entity\XApi\Cmi5Item
*
* @Gedmo\TreeRoot()
* @ORM\ManyToOne(targetEntity="Chamilo\PluginBundle\Entity\XApi\Cmi5Item")
* @ORM\JoinColumn(name="tree_root", referencedColumnName="id", onDelete="CASCADE")
*/
private $root;
/**
* @var \Chamilo\PluginBundle\Entity\XApi\Cmi5Item|null
*
* @Gedmo\TreeParent()
* @ORM\ManyToOne(targetEntity="Chamilo\PluginBundle\Entity\XApi\Cmi5Item", inversedBy="children")
* @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE")
*/
private $parent;
/**
* @var \Doctrine\Common\Collections\ArrayCollection
*
* @ORM\OneToMany(targetEntity="Chamilo\PluginBundle\Entity\XApi\Cmi5Item", mappedBy="parent")
* @ORM\OrderBy({"lft"="ASC"})
*/
private $children;
/**
* Cmi5Item constructor.
*/
public function __construct()
{
$this->children = new ArrayCollection();
}
public function getId(): int
{
return $this->id;
}
public function setId(int $id): Cmi5Item
{
$this->id = $id;
return $this;
}
public function getIdentifier(): string
{
return $this->identifier;
}
public function setIdentifier(string $identifier): Cmi5Item
{
$this->identifier = $identifier;
return $this;
}
public function getType(): string
{
return $this->type;
}
public function setType(string $type): Cmi5Item
{
$this->type = $type;
return $this;
}
public function getTitle(): array
{
return $this->title;
}
public function setTitle(array $title): Cmi5Item
{
$this->title = $title;
return $this;
}
public function getDescription(): array
{
return $this->description;
}
public function setDescription(array $description): Cmi5Item
{
$this->description = $description;
return $this;
}
public function getUrl(): ?string
{
return $this->url;
}
public function setUrl(?string $url): Cmi5Item
{
$this->url = $url;
return $this;
}
public function getActivityType(): ?string
{
return $this->activityType;
}
public function setActivityType(?string $activityType): Cmi5Item
{
$this->activityType = $activityType;
return $this;
}
public function getLaunchMethod(): ?string
{
return $this->launchMethod;
}
public function setLaunchMethod(?string $launchMethod): Cmi5Item
{
$this->launchMethod = $launchMethod;
return $this;
}
public function getMoveOn(): ?string
{
return $this->moveOn;
}
public function setMoveOn(?string $moveOn): Cmi5Item
{
$this->moveOn = $moveOn;
return $this;
}
public function getMasteryScore(): ?float
{
return $this->masteryScore;
}
public function setMasteryScore(?float $masteryScore): Cmi5Item
{
$this->masteryScore = $masteryScore;
return $this;
}
public function getLaunchParameters(): ?string
{
return $this->launchParameters;
}
public function setLaunchParameters(?string $launchParameters): Cmi5Item
{
$this->launchParameters = $launchParameters;
return $this;
}
public function getEntitlementKey(): ?string
{
return $this->entitlementKey;
}
public function setEntitlementKey(?string $entitlementKey): Cmi5Item
{
$this->entitlementKey = $entitlementKey;
return $this;
}
/**
* @return \Chamilo\PluginBundle\Entity\XApi\Cmi5Item|null
*/
public function getParent(): ?Cmi5Item
{
return $this->parent;
}
/**
* @param \Chamilo\PluginBundle\Entity\XApi\Cmi5Item|null $parent
*/
public function setParent(?Cmi5Item $parent): Cmi5Item
{
$this->parent = $parent;
return $this;
}
public function getChildren(): ArrayCollection
{
return $this->children;
}
public function setChildren(ArrayCollection $children): Cmi5Item
{
$this->children = $children;
return $this;
}
public function getStatus(): ?string
{
return $this->status;
}
public function setStatus(?string $status): Cmi5Item
{
$this->status = $status;
return $this;
}
/**
* @return \Chamilo\PluginBundle\Entity\XApi\ToolLaunch
*/
public function getTool(): ToolLaunch
{
return $this->tool;
}
/**
* @param \Chamilo\PluginBundle\Entity\XApi\ToolLaunch $tool
*/
public function setTool(ToolLaunch $tool): Cmi5Item
{
$this->tool = $tool;
return $this;
}
/**
* @return \Chamilo\PluginBundle\Entity\XApi\Cmi5Item
*/
public function getRoot(): Cmi5Item
{
return $this->root;
}
}

View File

@@ -0,0 +1,227 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Entity\XApi;
use Chamilo\UserBundle\Entity\User;
use DateTime;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Table(name="xapi_internal_log")
* @ORM\Entity()
*/
class InternalLog
{
/**
* @var int
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id()
* @ORM\GeneratedValue()
*/
private $id;
/**
* @ORM\ManyToOne(targetEntity="Chamilo\UserBundle\Entity\User")
* @ORM\JoinColumn(name="user_id", referencedColumnName="id", onDelete="CASCADE")
*/
private $user;
/**
* @var string
*
* @ORM\Column(name="statement_id", type="string")
*/
private $statementId;
/**
* @var string
*
* @ORM\Column(name="verb", type="string")
*/
private $verb;
/**
* @var string
*
* @ORM\Column(name="object_id", type="string")
*/
private $objectId;
/**
* @var string|null
*
* @ORM\Column(name="activity_name", type="string", nullable=true)
*/
private $activityName;
/**
* @var string|null
*
* @ORM\Column(name="activity_description", type="string", nullable=true)
*/
private $activityDescription;
/**
* @var float|null
*
* @ORM\Column(name="score_scaled", type="float", nullable=true)
*/
private $scoreScaled;
/**
* @var float|null
*
* @ORM\Column(name="score_raw", type="float", nullable=true)
*/
private $scoreRaw;
/**
* @var float|null
*
* @ORM\Column(name="score_min", type="float", nullable=true)
*/
private $scoreMin;
/**
* @var float|null
*
* @ORM\Column(name="score_max", type="float", nullable=true)
*/
private $scoreMax;
/**
* @var DateTime|null
*
* @ORM\Column(name="created_at", type="datetime", nullable=true)
*/
private $createdAt;
public function getId(): int
{
return $this->id;
}
public function getUser(): User
{
return $this->user;
}
public function setUser(User $user): InternalLog
{
$this->user = $user;
return $this;
}
public function getStatementId(): string
{
return $this->statementId;
}
public function setStatementId(string $statementId): InternalLog
{
$this->statementId = $statementId;
return $this;
}
public function getVerb(): string
{
return $this->verb;
}
public function setVerb(string $verb): InternalLog
{
$this->verb = $verb;
return $this;
}
public function getObjectId(): string
{
return $this->objectId;
}
public function setObjectId(string $objectId): InternalLog
{
$this->objectId = $objectId;
return $this;
}
public function getActivityName(): ?string
{
return $this->activityName;
}
public function setActivityName(?string $activityName): InternalLog
{
$this->activityName = $activityName;
return $this;
}
public function getActivityDescription(): ?string
{
return $this->activityDescription;
}
public function setActivityDescription(?string $activityDescription): InternalLog
{
$this->activityDescription = $activityDescription;
return $this;
}
public function getScoreScaled(): ?float
{
return $this->scoreScaled;
}
public function setScoreScaled(?float $scoreScaled): InternalLog
{
$this->scoreScaled = $scoreScaled;
return $this;
}
public function getScoreRaw(): ?float
{
return $this->scoreRaw;
}
public function setScoreRaw(?float $scoreRaw): InternalLog
{
$this->scoreRaw = $scoreRaw;
return $this;
}
public function getScoreMin(): ?float
{
return $this->scoreMin;
}
public function setScoreMin(?float $scoreMin): InternalLog
{
$this->scoreMin = $scoreMin;
return $this;
}
public function getScoreMax(): ?float
{
return $this->scoreMax;
}
public function setScoreMax(?float $scoreMax): InternalLog
{
$this->scoreMax = $scoreMax;
return $this;
}
public function getCreatedAt(): ?DateTime
{
return $this->createdAt;
}
public function setCreatedAt(?DateTime $createdAt): InternalLog
{
$this->createdAt = $createdAt;
return $this;
}
}

View File

@@ -0,0 +1,104 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Entity\XApi;
use Doctrine\ORM\Mapping as ORM;
/**
* Class LrsAuth.
*
* @package Chamilo\PluginBundle\Entity\XApi
*
* @ORM\Table(name="xapi_lrs_auth")
* @ORM\Entity()
*/
class LrsAuth
{
/**
* @var int
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id()
* @ORM\GeneratedValue()
*/
private $id;
/**
* @var string
*
* @ORM\Column(name="username", type="string")
*/
private $username;
/**
* @var string
*
* @ORM\Column(name="password", type="string")
*/
private $password;
/**
* @var bool
*
* @ORM\Column(name="enabled", type="boolean")
*/
private $enabled;
/**
* @var \DateTime
*
* @ORM\Column(name="created_at", type="datetime")
*/
private $createdAt;
public function getId(): int
{
return $this->id;
}
public function getUsername(): string
{
return $this->username;
}
public function setUsername(string $username): LrsAuth
{
$this->username = $username;
return $this;
}
public function getPassword(): string
{
return $this->password;
}
public function setPassword(string $password): LrsAuth
{
$this->password = $password;
return $this;
}
public function isEnabled(): bool
{
return $this->enabled;
}
public function setEnabled(bool $enabled): LrsAuth
{
$this->enabled = $enabled;
return $this;
}
public function getCreatedAt(): \DateTime
{
return $this->createdAt;
}
public function setCreatedAt(\DateTime $createdAt): LrsAuth
{
$this->createdAt = $createdAt;
return $this;
}
}

View File

@@ -0,0 +1,85 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Entity\XApi\Repository;
use Chamilo\CoreBundle\Entity\Course;
use Chamilo\CoreBundle\Entity\Session;
use Chamilo\CourseBundle\Entity\CLp;
use Chamilo\CourseBundle\Entity\CLpItem;
use Chamilo\PluginBundle\Entity\XApi\ToolLaunch;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Query\Expr\Join;
/**
* Class ToolLaunchRepository.
*
* @package Chamilo\PluginBundle\Entity\XApi\Repository
*/
class ToolLaunchRepository extends EntityRepository
{
public function findByCourseAndSession(
Course $course,
Session $session = null,
array $orderBy = [],
int $limit = null,
int $start = null
): array {
$criteria = [
'course' => $course,
'session' => null,
];
if ($session) {
$criteria['session'] = $session;
}
return $this->findBy($criteria, $orderBy, $limit, $start);
}
public function countByCourseAndSession(Course $course, Session $session = null, $filteredForStudent = false): int
{
$qb = $this->createQueryBuilder('tl');
$qb->select($qb->expr()->count('tl'))
->where($qb->expr()->eq('tl.course', ':course'))
->setParameter('course', $course);
if ($session) {
$qb->andWhere($qb->expr()->eq('tl.session', ':session'))
->setParameter('session', $session);
} else {
$qb->andWhere($qb->expr()->isNull('tl.session'));
}
if ($filteredForStudent) {
$qb
->leftJoin(
CLpItem::class,
'lpi',
Join::WITH,
"tl.id = lpi.path AND tl.course = lpi.cId AND lpi.itemType = 'xapi'"
)
->andWhere($qb->expr()->isNull('lpi.path'));
}
$query = $qb->getQuery();
return (int) $query->getSingleScalarResult();
}
public function wasAddedInLp(ToolLaunch $toolLaunch): int
{
$qb = $this->getEntityManager()->createQueryBuilder();
return (int) $qb->select($qb->expr()->count('lp'))
->from(CLp::class, 'lp')
->innerJoin(CLpItem::class, 'lpi', Join::WITH, 'lp.id = lpi.lpId')
->where('lpi.itemType = :type')
->andWhere('lpi.path = :tool_id')
->setParameter('type', TOOL_XAPI)
->setParameter('tool_id', $toolLaunch->getId())
->getQuery()
->getSingleScalarResult();
}
}

View File

@@ -0,0 +1,105 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Entity\XApi;
use Doctrine\ORM\Mapping as ORM;
/**
* Class SharedStatement.
*
* @package Chamilo\PluginBundle\Entity\XApi
*
* @ORM\Table(
* name="xapi_shared_statement",
* indexes={
* @ORM\Index(name="idx_uuid", columns={"uuid"})
* }
* )
* @ORM\Entity()
*/
class SharedStatement
{
/**
* @var int
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id()
* @ORM\GeneratedValue()
*/
private $id;
/**
* @var string|null
*
* @ORM\Column(name="uuid", type="string", nullable=true)
*/
private $uuid;
/**
* @var array
*
* @ORM\Column(name="statement", type="array")
*/
private $statement;
/**
* @var bool
*
* @ORM\Column(name="sent", type="boolean", options={"default":false})
*/
private $sent;
/**
* SharedStatement constructor.
*
* @param array $statement
* @param null $uuid
* @param false $sent
*/
public function __construct($statement, $uuid = null, $sent = false)
{
$this->statement = $statement;
$this->uuid = $uuid;
$this->sent = $sent;
}
public function getId(): int
{
return $this->id;
}
public function getUuid(): ?string
{
return $this->uuid;
}
public function setUuid(?string $uuid): SharedStatement
{
$this->uuid = $uuid;
return $this;
}
public function getStatement(): array
{
return $this->statement;
}
public function setStatement(array $statement): SharedStatement
{
$this->statement = $statement;
return $this;
}
public function isSent(): bool
{
return $this->sent;
}
public function setSent(bool $sent): SharedStatement
{
$this->sent = $sent;
return $this;
}
}

View File

@@ -0,0 +1,295 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Entity\XApi;
use Chamilo\CoreBundle\Entity\Course;
use Chamilo\CoreBundle\Entity\Session;
use DateTime;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
/**
* Class ToolLaunch.
*
* @package Chamilo\PluginBundle\Entity\XApi
*
* @ORM\Table(name="xapi_tool_launch")
* @ORM\Entity(repositoryClass="Chamilo\PluginBundle\Entity\XApi\Repository\ToolLaunchRepository")
*/
class ToolLaunch
{
/**
* @var int
*
* @ORM\Column(type="integer", name="id")
* @ORM\Id()
* @ORM\GeneratedValue()
*/
private $id;
/**
* @var string
*
* @ORM\Column(name="title", type="string")
*/
private $title;
/**
* @var string|null
*
* @ORM\Column(name="description", type="text", nullable=true)
*/
private $description;
/**
* @var \Chamilo\CoreBundle\Entity\Course
*
* @ORM\ManyToOne(targetEntity="Chamilo\CoreBundle\Entity\Course")
* @ORM\JoinColumn(name="c_id", referencedColumnName="id", nullable=false, onDelete="CASCADE")
*/
private $course;
/**
* @var \Chamilo\CoreBundle\Entity\Session|null
*
* @ORM\ManyToOne(targetEntity="Chamilo\CoreBundle\Entity\Session")
* @ORM\JoinColumn(name="session_id", referencedColumnName="id", onDelete="CASCADE")
*/
private $session;
/**
* @var string
*
* @ORM\Column(name="launch_url", type="string")
*/
private $launchUrl;
/**
* @var string|null
*
* @ORM\Column(name="activity_id", type="string", nullable=true)
*/
private $activityId;
/**
* @var string|null
*
* @ORM\Column(name="activity_type", type="string", nullable=true)
*/
private $activityType;
/**
* @var bool
*
* @ORM\Column(name="allow_multiple_attempts", type="boolean", options={"default": true})
*/
private $allowMultipleAttempts;
/**
* @var string|null
*
* @ORM\Column(name="lrs_url", type="string", nullable=true)
*/
private $lrsUrl;
/**
* @var string|null
*
* @ORM\Column(name="lrs_auth_username", type="string", nullable=true)
*/
private $lrsAuthUsername;
/**
* @var string|null
*
* @ORM\Column(name="lrs_auth_password", type="string", nullable=true)
*/
private $lrsAuthPassword;
/*
* @var \DateTime
*
* @ORM\Column(name="created_at", type="datetime")
*/
private $createdAt;
/**
* @var \Doctrine\Common\Collections\ArrayCollection
*
* @ORM\OneToMany(targetEntity="Chamilo\PluginBundle\Entity\XApi\Cmi5Item", mappedBy="tool", orphanRemoval=true,
* cascade="ALL")
*/
private $items;
/**
* ToolLaunch constructor.
*/
public function __construct()
{
$this->allowMultipleAttempts = true;
$this->items = new ArrayCollection();
}
public function getId(): int
{
return $this->id;
}
public function setId(int $id): ToolLaunch
{
$this->id = $id;
return $this;
}
public function getTitle(): string
{
return $this->title;
}
public function setTitle(string $title): ToolLaunch
{
$this->title = $title;
return $this;
}
public function getDescription(): ?string
{
return $this->description;
}
public function setDescription(?string $description): ToolLaunch
{
$this->description = $description;
return $this;
}
public function getCourse(): Course
{
return $this->course;
}
public function setCourse(Course $course): ToolLaunch
{
$this->course = $course;
return $this;
}
public function getSession(): ?Session
{
return $this->session;
}
public function setSession(?Session $session): ToolLaunch
{
$this->session = $session;
return $this;
}
public function getLaunchUrl(): string
{
return $this->launchUrl;
}
public function setLaunchUrl(string $launchUrl): ToolLaunch
{
$this->launchUrl = $launchUrl;
return $this;
}
public function getActivityId(): ?string
{
return $this->activityId;
}
public function setActivityId(?string $activityId): ToolLaunch
{
$this->activityId = $activityId;
return $this;
}
public function getCreatedAt(): DateTime
{
return $this->createdAt;
}
public function setCreatedAt(DateTime $createdAt): ToolLaunch
{
$this->createdAt = $createdAt;
return $this;
}
public function getActivityType(): ?string
{
return $this->activityType;
}
public function setActivityType(?string $activityType): ToolLaunch
{
$this->activityType = $activityType;
return $this;
}
public function isAllowMultipleAttempts(): bool
{
return $this->allowMultipleAttempts;
}
public function setAllowMultipleAttempts(bool $allowMultipleAttempts): ToolLaunch
{
$this->allowMultipleAttempts = $allowMultipleAttempts;
return $this;
}
public function getLrsUrl(): ?string
{
return $this->lrsUrl;
}
public function setLrsUrl(?string $lrsUrl): ToolLaunch
{
$this->lrsUrl = $lrsUrl;
return $this;
}
public function getLrsAuthUsername(): ?string
{
return $this->lrsAuthUsername;
}
public function setLrsAuthUsername(?string $lrsAuthUsername): ToolLaunch
{
$this->lrsAuthUsername = $lrsAuthUsername;
return $this;
}
public function getLrsAuthPassword(): ?string
{
return $this->lrsAuthPassword;
}
public function setLrsAuthPassword(?string $lrsAuthPassword): ToolLaunch
{
$this->lrsAuthPassword = $lrsAuthPassword;
return $this;
}
public function getItems(): ArrayCollection
{
return $this->items;
}
/**
* @param \Chamilo\PluginBundle\Entity\XApi\Cmi5Item $cmi5Item
*
* @return $this
*/
public function addItem(Cmi5Item $cmi5Item)
{
$cmi5Item->setTool($this);
$this->items->add($cmi5Item);
return $this;
}
}

View File

@@ -0,0 +1,66 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\PluginBundle\Entity\XApi\SharedStatement;
use Xabbuh\XApi\Model\Statement;
use Xabbuh\XApi\Serializer\Symfony\Serializer;
use Xabbuh\XApi\Serializer\Symfony\StatementSerializer;
/**
* Class XApiActivityHookObserver.
*/
abstract class XApiActivityHookObserver extends HookObserver
{
/**
* @var \XApiPlugin
*/
protected $plugin;
/**
* XApiActivityHookObserver constructor.
*/
protected function __construct()
{
parent::__construct(
'plugin/xapi/src/XApiPlugin.php',
'xapi'
);
$this->plugin = XApiPlugin::create();
}
/**
* @throws \Doctrine\ORM\ORMException
* @throws \Doctrine\ORM\OptimisticLockException
*
* @return \Chamilo\PluginBundle\Entity\XApi\SharedStatement|null
*/
protected function saveSharedStatement(Statement $statement)
{
$statementSerialized = $this->serializeStatement($statement);
$sharedStmt = new SharedStatement(
json_decode($statementSerialized, true)
);
$em = Database::getManager();
$em->persist($sharedStmt);
$em->flush();
return $sharedStmt;
}
/**
* Serialize a statement to JSON.
*
* @return string
*/
private function serializeStatement(Statement $statement)
{
$serializer = Serializer::createSerializer();
$statementSerializer = new StatementSerializer($serializer);
return $statementSerializer->serializeStatement($statement);
}
}

View File

@@ -0,0 +1,37 @@
<?php
/* For licensing terms, see /license.txt */
/**
* Class XApiCreateCourseHookObserver.
*/
class XApiCreateCourseHookObserver extends HookObserver implements HookCreateCourseObserverInterface
{
/**
* XApiCreateCourseHookObserver constructor.
*/
protected function __construct()
{
parent::__construct(
'plugin/xapi/src/XApiPlugin.php',
'xapi'
);
}
/**
* {@inheritdoc}
*/
public function hookCreateCourse(HookCreateCourseEventInterface $hook)
{
$data = $hook->getEventData();
$type = $data['type'];
$courseInfo = $data['course_info'];
$plugin = XApiPlugin::create();
if (HOOK_EVENT_TYPE_POST == $type) {
$plugin->addCourseToolForTinCan($courseInfo['real_id']);
}
}
}

View File

@@ -0,0 +1,26 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\PluginBundle\XApi\ToolExperience\Statement\LearningPathCompleted;
/**
* Class XApiLearningPathEndHookObserver.
*/
class XApiLearningPathEndHookObserver extends XApiActivityHookObserver implements HookLearningPathEndObserverInterface
{
public function notifyLearningPathEnd(HookLearningPathEndEventInterface $event)
{
$data = $event->getEventData();
$em = Database::getManager();
$lpView = $em->find('ChamiloCourseBundle:CLpView', $data['lp_view_id']);
$lp = $em->find('ChamiloCourseBundle:CLp', $lpView->getLpId());
$learningPathEnded = new LearningPathCompleted($lpView, $lp);
$statement = $learningPathEnded->generate();
$this->saveSharedStatement($statement);
}
}

View File

@@ -0,0 +1,35 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\PluginBundle\XApi\ToolExperience\Statement\LearningPathItemViewed;
/**
* Class XApiLearningPathItemViewedHookObserver.
*/
class XApiLearningPathItemViewedHookObserver extends XApiActivityHookObserver implements HookLearningPathItemViewedObserverInterface
{
/**
* {@inheritdoc}
*/
public function hookLearningPathItemViewed(HookLearningPathItemViewedEventInterface $event)
{
$data = $event->getEventData();
$em = Database::getManager();
$lpItemView = $em->find('ChamiloCourseBundle:CLpItemView', $data['item_view_id']);
$lpItem = $em->find('ChamiloCourseBundle:CLpItem', $lpItemView->getLpItemId());
if ('quiz' == $lpItem->getItemType()) {
return null;
}
$lpView = $em->find('ChamiloCourseBundle:CLpView', $lpItemView->getLpViewId());
$lpItemViewed = new LearningPathItemViewed($lpItemView, $lpItem, $lpView);
$statement = $lpItemViewed->generate();
$this->saveSharedStatement($statement);
}
}

View File

@@ -0,0 +1,19 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\CoreBundle\Entity\PortfolioComment;
use Chamilo\PluginBundle\XApi\ToolExperience\Statement\PortfolioCommentEdited;
class XApiPortfolioCommentEditedHookObserver extends XApiActivityHookObserver implements HookPortfolioCommentEditedObserverInterface
{
public function hookCommentEdited(HookPortfolioCommentEditedEventInterface $hookEvent)
{
/** @var PortfolioComment $comment */
$comment = $hookEvent->getEventData()['comment'];
$statement = (new PortfolioCommentEdited($comment))->generate();
$this->saveSharedStatement($statement);
}
}

View File

@@ -0,0 +1,19 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\CoreBundle\Entity\PortfolioComment;
use Chamilo\PluginBundle\XApi\ToolExperience\Statement\PortfolioCommentScored;
class XApiPortfolioCommentScoredHookObserver extends XApiActivityHookObserver implements HookPortfolioCommentScoredObserverInterface
{
public function hookCommentScored(HookPortfolioCommentScoredEventInterface $hookEvent)
{
/** @var PortfolioComment $comment */
$comment = $hookEvent->getEventData()['comment'];
$statement = (new PortfolioCommentScored($comment))->generate();
$this->saveSharedStatement($statement);
}
}

View File

@@ -0,0 +1,17 @@
<?php
use Chamilo\PluginBundle\XApi\ToolExperience\Statement\PortfolioDownloaded;
use Chamilo\UserBundle\Entity\User;
class XApiPortfolioDownloadedHookObserver extends XApiActivityHookObserver implements HookPortfolioDownloadedObserverInterface
{
public function hookPortfolioDownloaded(HookPortfolioDownloadedEventInterface $hookEvent)
{
/** @var User $owner */
$owner = $hookEvent->getEventData()['owner'];
$statement = (new PortfolioDownloaded($owner))->generate();
$this->saveSharedStatement($statement);
}
}

View File

@@ -0,0 +1,25 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\PluginBundle\XApi\ToolExperience\Statement\PortfolioItemShared;
/**
* Class XApiPortfolioItemAddedHookObserver.
*/
class XApiPortfolioItemAddedHookObserver extends XApiActivityHookObserver implements HookPortfolioItemAddedObserverInterface
{
/**
* {@inheritDoc}
*/
public function hookItemAdded(HookPortfolioItemAddedEventInterface $hookEvent)
{
$item = $hookEvent->getEventData()['portfolio'];
$portfolioItemShared = new PortfolioItemShared($item);
$statement = $portfolioItemShared->generate();
$this->saveSharedStatement($statement);
}
}

View File

@@ -0,0 +1,25 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\PluginBundle\XApi\ToolExperience\Statement\PortfolioItemCommented;
/**
* Class XApiPortfolioItemCommentedHookObserver.
*/
class XApiPortfolioItemCommentedHookObserver extends XApiActivityHookObserver implements HookPortfolioItemCommentedObserverInterface
{
/**
* {@inheritDoc}
*/
public function hookItemCommented(HookPortfolioItemCommentedEventInterface $hookEvent)
{
$comment = $hookEvent->getEventData()['comment'];
$portfolioItemCommented = new PortfolioItemCommented($comment);
$statement = $portfolioItemCommented->generate();
$this->saveSharedStatement($statement);
}
}

View File

@@ -0,0 +1,19 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\CoreBundle\Entity\Portfolio;
use Chamilo\PluginBundle\XApi\ToolExperience\Statement\PortfolioItemEdited;
class XApiPortfolioItemEditedHookObserver extends XApiActivityHookObserver implements HookPortfolioItemEditedObserverInterface
{
public function hookItemEdited(HookPortfolioItemEditedEventInterface $hookEvent)
{
/** @var Portfolio $item */
$item = $hookEvent->getEventData()['item'];
$statement = (new PortfolioItemEdited($item))->generate();
$this->saveSharedStatement($statement);
}
}

View File

@@ -0,0 +1,22 @@
<?php
use Chamilo\CoreBundle\Entity\Portfolio;
use Doctrine\ORM\OptimisticLockException;
use Doctrine\ORM\ORMException;
class XApiPortfolioItemHighlightedHookObserver extends XApiActivityHookObserver implements HookPortfolioItemHighlightedObserverInterface
{
/**
* @throws OptimisticLockException
* @throws ORMException
*/
public function hookItemHighlighted(HookPortfolioItemHighlightedEventInterface $hookEvent)
{
/** @var Portfolio $item */
$item = $hookEvent->getEventData()['item'];
$statement = (new PortfolioItemHighlighted($item))->generate();
$this->saveSharedStatement($statement);
}
}

View File

@@ -0,0 +1,19 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\CoreBundle\Entity\Portfolio;
use Chamilo\PluginBundle\XApi\ToolExperience\Statement\PortfolioItemScored;
class XApiPortfolioItemScoredHookObserver extends XApiActivityHookObserver implements HookPortfolioItemScoredObserverInterface
{
public function hookItemScored(HookPortfolioItemScoredEventInterface $hookEvent)
{
/** @var Portfolio $item */
$item = $hookEvent->getEventData()['item'];
$statement = (new PortfolioItemScored($item))->generate();
$this->saveSharedStatement($statement);
}
}

View File

@@ -0,0 +1,25 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\CoreBundle\Entity\Portfolio;
use Chamilo\PluginBundle\XApi\ToolExperience\Statement\PortfolioItemViewed;
use Doctrine\ORM\OptimisticLockException;
use Doctrine\ORM\ORMException;
class XApiPortfolioItemViewedHookObserver extends XApiActivityHookObserver implements HookPortfolioItemViewedObserverInterface
{
/**
* @throws OptimisticLockException
* @throws ORMException
*/
public function hookItemViewed(HookPortfolioItemViewedEventInterface $hookEvent)
{
/** @var Portfolio $item */
$item = $hookEvent->getEventData()['portfolio'];
$statement = (new PortfolioItemViewed($item))->generate();
$this->saveSharedStatement($statement);
}
}

View File

@@ -0,0 +1,29 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\PluginBundle\XApi\ToolExperience\Statement\QuizCompleted;
/**
* Class XApiQuizEndHookObserver.
*/
class XApiQuizEndHookObserver extends XApiActivityHookObserver implements HookQuizEndObserverInterface
{
/**
* {@inheritdoc}
*/
public function hookQuizEnd(HookQuizEndEventInterface $hookEvent)
{
$data = $hookEvent->getEventData();
$em = Database::getManager();
$exe = $em->find('ChamiloCoreBundle:TrackEExercises', $data['exe_id']);
$quiz = $em->find('ChamiloCourseBundle:CQuiz', $exe->getExeExoId());
$quizCompleted = new QuizCompleted($exe, $quiz);
$statement = $quizCompleted->generate();
$this->saveSharedStatement($statement);
}
}

View File

@@ -0,0 +1,42 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\CoreBundle\Entity\TrackEAttempt;
use Chamilo\CoreBundle\Entity\TrackEExercises;
use Chamilo\CourseBundle\Entity\CQuiz;
use Chamilo\CourseBundle\Entity\CQuizQuestion;
use Chamilo\PluginBundle\XApi\ToolExperience\Statement\QuizQuestionAnswered;
/**
* Class XApiQuizQuestionAnsweredHook.
*/
class XApiQuizQuestionAnsweredHookObserver extends XApiActivityHookObserver implements HookQuizQuestionAnsweredObserverInterface
{
/**
* {@inheritdoc}
*/
public function hookQuizQuestionAnswered(HookQuizQuestionAnsweredEventInterface $event)
{
$data = $event->getEventData();
$em = Database::getManager();
$attemptRepo = $em->getRepository(TrackEAttempt::class);
$exe = $em->find(TrackEExercises::class, $data['exe_id']);
$question = $em->find(CQuizQuestion::class, $data['question']['id']);
$attempt = $attemptRepo->findOneBy(
[
'exeId' => $exe->getExeId(),
'questionId' => $question->getId(),
]
);
$quiz = $em->find(CQuiz::class, $data['quiz']['id']);
$quizQuestionAnswered = new QuizQuestionAnswered($attempt, $question, $quiz);
$statement = $quizQuestionAnswered->generate();
$this->saveSharedStatement($statement);
}
}

View File

@@ -0,0 +1,67 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\XApi\Importer;
use Chamilo\CoreBundle\Entity\Course;
/**
* Class AbstractImporter.
*
* @package Chamilo\PluginBundle\XApi\Importer
*/
abstract class PackageImporter
{
/**
* @var \Chamilo\CoreBundle\Entity\Course
*/
protected $course;
/**
* @var string
*/
protected $courseDirectoryPath;
/**
* @var array
*/
protected $packageFileInfo;
/**
* @var string
*/
protected $packageType;
/**
* AbstractImporter constructor.
*/
protected function __construct(array $fileInfo, Course $course)
{
$this->packageFileInfo = $fileInfo;
$this->course = $course;
$this->courseDirectoryPath = api_get_path(SYS_COURSE_PATH).$this->course->getDirectory();
}
/**
* @return \Chamilo\PluginBundle\XApi\Importer\XmlPackageImporter|\Chamilo\PluginBundle\XApi\Importer\ZipPackageImporter
*/
public static function create(array $fileInfo, Course $course)
{
if ('text/xml' === $fileInfo['type']) {
return new XmlPackageImporter($fileInfo, $course);
}
return new ZipPackageImporter($fileInfo, $course);
}
/**
* @throws \Exception
*
* @return mixed
*/
abstract public function import(): string;
public function getPackageType(): string
{
return $this->packageType;
}
}

View File

@@ -0,0 +1,29 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\XApi\Importer;
use Exception;
/**
* Class XmlImporter.
*
* @package Chamilo\PluginBundle\XApi\Importer
*/
class XmlPackageImporter extends PackageImporter
{
/**
* {@inheritDoc}
*/
public function import(): string
{
if (!in_array($this->packageFileInfo['name'], ['tincan.xml', 'cmi5.xml'])) {
throw new Exception('Invalid package');
}
$this->packageType = explode('.', $this->packageFileInfo['name'], 2)[0];
return $this->packageFileInfo['tmp_name'];
}
}

View File

@@ -0,0 +1,88 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\XApi\Importer;
use DocumentManager;
use Exception;
use PclZip;
use Symfony\Component\Filesystem\Filesystem;
/**
* Class ZipImporter.
*
* @package Chamilo\PluginBundle\XApi\Importer
*/
class ZipPackageImporter extends PackageImporter
{
/**
* {@inheritDoc}
*/
public function import(): string
{
$zipFile = new PclZip($this->packageFileInfo['tmp_name']);
$zipContent = $zipFile->listContent();
$packageSize = array_reduce(
$zipContent,
function ($accumulator, $zipEntry) {
if (preg_match('~.(php.*|phtml)$~i', $zipEntry['filename'])) {
throw new Exception("File \"{$zipEntry['filename']}\" contains a PHP script");
}
if (in_array($zipEntry['filename'], ['tincan.xml', 'cmi5.xml'])) {
$this->packageType = explode('.', $zipEntry['filename'], 2)[0];
}
return $accumulator + $zipEntry['size'];
}
);
if (empty($this->packageType)) {
throw new Exception('Invalid package');
}
$this->validateEnoughSpace($packageSize);
$pathInfo = pathinfo($this->packageFileInfo['name']);
$packageDirectoryPath = $this->generatePackageDirectory($pathInfo['filename']);
$zipFile->extract($packageDirectoryPath);
return "$packageDirectoryPath/{$this->packageType}.xml";
}
/**
* @throws \Exception
*/
protected function validateEnoughSpace(int $packageSize)
{
$courseSpaceQuota = DocumentManager::get_course_quota($this->course->getCode());
if (!enough_size($packageSize, $this->courseDirectoryPath, $courseSpaceQuota)) {
throw new Exception('Not enough space to storage package.');
}
}
private function generatePackageDirectory(string $name): string
{
$directoryPath = implode(
'/',
[
$this->courseDirectoryPath,
$this->packageType,
api_replace_dangerous_char($name),
]
);
$fs = new Filesystem();
$fs->mkdir(
$directoryPath,
api_get_permissions_for_new_directories()
);
return $directoryPath;
}
}

View File

@@ -0,0 +1,30 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\XApi\Lrs;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
/**
* Class AboutController.
*
* @package Chamilo\PluginBundle\XApi\Lrs
*/
class AboutController extends BaseController
{
public function get(): Response
{
$json = [
'version' => [
'1.0.3',
'1.0.2',
'1.0.1',
'1.0.0',
],
];
return JsonResponse::create($json);
}
}

View File

@@ -0,0 +1,78 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\XApi\Lrs;
use Chamilo\PluginBundle\Entity\XApi\ActivityProfile;
use Symfony\Component\HttpFoundation\Response;
/**
* Class ActivitiesProfileController.
*
* @package Chamilo\PluginBundle\XApi\Lrs
*/
class ActivitiesProfileController extends BaseController
{
public function get(): Response
{
$profileId = $this->httpRequest->query->get('profileId');
$activityId = $this->httpRequest->query->get('activityId');
$em = \Database::getManager();
$profileRepo = $em->getRepository(ActivityProfile::class);
/** @var ActivityProfile $activityProfile */
$activityProfile = $profileRepo->findOneBy(
[
'profileId' => $profileId,
'activityId' => $activityId,
]
);
if (empty($activityProfile)) {
return Response::create(null, Response::HTTP_NO_CONTENT);
}
return Response::create(
json_encode($activityProfile->getDocumentData())
);
}
public function head(): Response
{
return $this->get()->setContent('');
}
public function put(): Response
{
$profileId = $this->httpRequest->query->get('profileId');
$activityId = $this->httpRequest->query->get('activityId');
$documentData = $this->httpRequest->getContent();
$em = \Database::getManager();
$profileRepo = $em->getRepository(ActivityProfile::class);
/** @var ActivityProfile $activityProfile */
$activityProfile = $profileRepo->findOneBy(
[
'profileId' => $profileId,
'activityId' => $activityId,
]
);
if (empty($activityProfile)) {
$activityProfile = new ActivityProfile();
$activityProfile
->setProfileId($profileId)
->setActivityId($activityId);
}
$activityProfile->setDocumentData(json_decode($documentData, true));
$em->persist($activityProfile);
$em->flush();
return Response::create(null, Response::HTTP_NO_CONTENT);
}
}

View File

@@ -0,0 +1,120 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\XApi\Lrs;
use Chamilo\PluginBundle\Entity\XApi\ActivityState;
use Database;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Xabbuh\XApi\Model\Actor;
use Xabbuh\XApi\Serializer\Symfony\Serializer;
/**
* Class ActivitiesStateController.
*
* @package Chamilo\PluginBundle\XApi\Lrs
*/
class ActivitiesStateController extends BaseController
{
public function get(): Response
{
$serializer = Serializer::createSerializer();
$requestedAgent = $this->httpRequest->query->get('agent');
$activityId = $this->httpRequest->query->get('activityId');
$stateId = $this->httpRequest->query->get('stateId');
$state = Database::select(
'*',
Database::get_main_table('xapi_activity_state'),
[
'where' => [
'state_id = ? AND activity_id = ? AND MD5(agent) = ?' => [
Database::escape_string($stateId),
Database::escape_string($activityId),
md5($requestedAgent),
],
],
],
'first'
);
if (empty($state)) {
return JsonResponse::create([], Response::HTTP_NOT_FOUND);
}
$requestedAgent = $serializer->deserialize(
$this->httpRequest->query->get('agent'),
Actor::class,
'json'
);
/** @var Actor $stateAgent */
$stateAgent = $serializer->deserialize(
$state['agent'],
Actor::class,
'json'
);
if (!$stateAgent->equals($requestedAgent)) {
return JsonResponse::create([], Response::HTTP_NOT_FOUND);
}
$documentData = json_decode($state['document_data'], true);
return JsonResponse::create($documentData);
}
public function head(): Response
{
return $this->get()->setContent('');
}
public function post(): Response
{
return $this->put();
}
public function put(): Response
{
$activityId = $this->httpRequest->query->get('activityId');
$agent = $this->httpRequest->query->get('agent');
$stateId = $this->httpRequest->query->get('stateId');
$documentData = $this->httpRequest->getContent();
$state = Database::select(
'id',
Database::get_main_table('xapi_activity_state'),
[
'where' => [
'state_id = ? AND activity_id = ? AND MD5(agent) = ?' => [
Database::escape_string($stateId),
Database::escape_string($activityId),
md5($agent),
],
],
],
'first'
);
$em = Database::getManager();
if (empty($state)) {
$state = new ActivityState();
$state
->setActivityId($activityId)
->setAgent(json_decode($agent, true))
->setStateId($stateId);
} else {
$state = $em->find(ActivityState::class, $state['id']);
}
$state->setDocumentData(json_decode($documentData, true));
$em->persist($state);
$em->flush();
return Response::create('', Response::HTTP_NO_CONTENT);
}
}

View File

@@ -0,0 +1,28 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\XApi\Lrs;
use Symfony\Component\HttpFoundation\Request;
/**
* Class BaseController.
*
* @package Chamilo\PluginBundle\XApi\Lrs
*/
abstract class BaseController
{
/**
* @var \Symfony\Component\HttpFoundation\Request
*/
protected $httpRequest;
/**
* BaseController constructor.
*/
public function __construct(Request $httpRequest)
{
$this->httpRequest = $httpRequest;
}
}

View File

@@ -0,0 +1,221 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\XApi\Lrs;
use Chamilo\PluginBundle\Entity\XApi\LrsAuth;
use Database;
use Symfony\Component\HttpFoundation\Request as HttpRequest;
use Symfony\Component\HttpFoundation\Response as HttpResponse;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Xabbuh\XApi\Common\Exception\AccessDeniedException;
use Xabbuh\XApi\Common\Exception\XApiException;
/**
* Class LrsRequest.
*
* @package Chamilo\PluginBundle\XApi\Lrs
*/
class LrsRequest
{
/**
* @var HttpRequest
*/
private $request;
/**
* LrsRequest constructor.
*/
public function __construct()
{
$this->request = HttpRequest::createFromGlobals();
}
public function send()
{
try {
$this->alternateRequestSyntax();
$controllerName = $this->getControllerName();
$methodName = $this->getMethodName();
$response = $this->generateResponse($controllerName, $methodName);
} catch (XApiException $xApiException) {
$response = HttpResponse::create('', HttpResponse::HTTP_BAD_REQUEST);
} catch (HttpException $httpException) {
$response = HttpResponse::create(
$httpException->getMessage(),
$httpException->getStatusCode()
);
} catch (\Exception $exception) {
$response = HttpResponse::create($exception->getMessage(), HttpResponse::HTTP_BAD_REQUEST);
}
$response->headers->set('X-Experience-API-Version', '1.0.3');
$response->send();
}
/**
* @throws \Xabbuh\XApi\Common\Exception\AccessDeniedException
*/
private function validateAuth(): bool
{
if (!$this->request->headers->has('Authorization')) {
throw new AccessDeniedException();
}
$authHeader = $this->request->headers->get('Authorization');
$parts = explode('Basic ', $authHeader, 2);
if (empty($parts[1])) {
throw new AccessDeniedException();
}
$authDecoded = base64_decode($parts[1]);
$parts = explode(':', $authDecoded, 2);
if (empty($parts) || count($parts) !== 2) {
throw new AccessDeniedException();
}
list($username, $password) = $parts;
$auth = Database::getManager()
->getRepository(LrsAuth::class)
->findOneBy(
['username' => $username, 'password' => $password, 'enabled' => true]
);
if (null == $auth) {
throw new AccessDeniedException();
}
return true;
}
private function validateVersion()
{
$version = $this->request->headers->get('X-Experience-API-Version');
if (null === $version) {
throw new BadRequestHttpException('The "X-Experience-API-Version" header is required.');
}
if (preg_match('/^1\.0(?:\.\d+)?$/', $version)) {
if ('1.0' === $version) {
$this->request->headers->set('X-Experience-API-Version', '1.0.0');
}
return;
}
throw new BadRequestHttpException("The xAPI version \"$version\" is not supported.");
}
private function getControllerName(): ?string
{
$segments = explode('/', $this->request->getPathInfo());
$segments = array_filter($segments);
$segments = array_values($segments);
if (empty($segments)) {
throw new BadRequestHttpException('Bad request');
}
$segments = array_map('ucfirst', $segments);
$controllerName = implode('', $segments).'Controller';
return "Chamilo\\PluginBundle\\XApi\Lrs\\$controllerName";
}
private function getMethodName(): string
{
$method = $this->request->getMethod();
return strtolower($method);
}
/**
* @throws \Xabbuh\XApi\Common\Exception\AccessDeniedException
*/
private function generateResponse(string $controllerName, string $methodName): HttpResponse
{
if (!class_exists($controllerName)
|| !method_exists($controllerName, $methodName)
) {
throw new NotFoundHttpException();
}
if ($controllerName !== AboutController::class) {
$this->validateAuth();
$this->validateVersion();
}
/** @var HttpResponse $response */
$response = call_user_func(
[
new $controllerName($this->request),
$methodName,
]
);
return $response;
}
private function alternateRequestSyntax()
{
if ('POST' !== $this->request->getMethod()) {
return;
}
if (null === $method = $this->request->query->get('method')) {
return;
}
if ($this->request->query->count() > 1) {
throw new BadRequestHttpException('Including other query parameters than "method" is not allowed. You have to send them as POST parameters inside the request body.');
}
$this->request->setMethod($method);
$this->request->query->remove('method');
if (null !== $content = $this->request->request->get('content')) {
$this->request->request->remove('content');
$this->request->initialize(
$this->request->query->all(),
$this->request->request->all(),
$this->request->attributes->all(),
$this->request->cookies->all(),
$this->request->files->all(),
$this->request->server->all(),
$content
);
}
$headerNames = [
'Authorization',
'X-Experience-API-Version',
'Content-Type',
'Content-Length',
'If-Match',
'If-None-Match',
];
foreach ($this->request->request as $key => $value) {
if (in_array($key, $headerNames, true)) {
$this->request->headers->set($key, $value);
} else {
$this->request->query->set($key, $value);
}
$this->request->request->remove($key);
}
}
}

View File

@@ -0,0 +1,147 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\XApi\Lrs;
use Chamilo\PluginBundle\XApi\Lrs\Util\InternalLogUtil;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Xabbuh\XApi\Common\Exception\NotFoundException;
use Xabbuh\XApi\Model\StatementId;
use Xabbuh\XApi\Serializer\Symfony\ActorSerializer;
use Xabbuh\XApi\Serializer\Symfony\Serializer;
use Xabbuh\XApi\Serializer\Symfony\SerializerFactory;
use XApi\LrsBundle\Controller\StatementGetController;
use XApi\LrsBundle\Controller\StatementHeadController;
use XApi\LrsBundle\Controller\StatementPostController;
use XApi\LrsBundle\Controller\StatementPutController;
use XApi\LrsBundle\Model\StatementsFilterFactory;
use XApi\Repository\Doctrine\Mapping\Statement as StatementEntity;
use XApi\Repository\Doctrine\Repository\StatementRepository;
use XApiPlugin;
/**
* Class StatementsController.
*
* @package Chamilo\PluginBundle\XApi\Lrs
*/
class StatementsController extends BaseController
{
/**
* @var StatementRepository
*/
private $statementRepository;
/**
* @var \Symfony\Component\Serializer\Serializer|\Symfony\Component\Serializer\SerializerInterface
*/
private $serializer;
/**
* @var SerializerFactory
*/
private $serializerFactory;
public function __construct(Request $httpRequest)
{
parent::__construct($httpRequest);
$pluginEm = XApiPlugin::getEntityManager();
$this->statementRepository = new StatementRepository(
$pluginEm->getRepository(StatementEntity::class)
);
$this->serializer = Serializer::createSerializer();
$this->serializerFactory = new SerializerFactory($this->serializer);
}
public function get(): Response
{
$getStatementController = new StatementGetController(
$this->statementRepository,
$this->serializerFactory->createStatementSerializer(),
$this->serializerFactory->createStatementResultSerializer(),
new StatementsFilterFactory(
new ActorSerializer($this->serializer)
)
);
return $getStatementController->getStatement($this->httpRequest);
}
public function head(): Response
{
$headStatementController = new StatementHeadController(
$this->statementRepository,
$this->serializerFactory->createStatementSerializer(),
$this->serializerFactory->createStatementResultSerializer(),
new StatementsFilterFactory(
new ActorSerializer($this->serializer)
)
);
return $headStatementController->getStatement($this->httpRequest);
}
public function put(): Response
{
$statement = $this->serializerFactory
->createStatementSerializer()
->deserializeStatement(
$this->httpRequest->getContent()
)
;
$putStatementController = new StatementPutController($this->statementRepository);
$response = $putStatementController->putStatement($this->httpRequest, $statement);
$this->saveLog(
[$this->httpRequest->query->get('statementId')]
);
return $response;
}
public function post(): Response
{
$content = $this->httpRequest->getContent();
if (substr($content, 0, 1) !== '[') {
$content = "[$content]";
}
$statements = $this->serializerFactory
->createStatementSerializer()
->deserializeStatements($content)
;
$postStatementController = new StatementPostController($this->statementRepository);
$response = $postStatementController->postStatements($this->httpRequest, $statements);
$this->saveLog(
json_decode($response->getContent(), false)
);
return $response;
}
/**
* @param array<string> $statementsId
*
* @return void
*/
private function saveLog(array $statementsId)
{
foreach ($statementsId as $statementId) {
try {
$storedStatement = $this->statementRepository->findStatementById(
StatementId::fromString($statementId)
);
InternalLogUtil::saveStatementForInternalLog($storedStatement);
} catch (NotFoundException $e) {
}
}
}
}

View File

@@ -0,0 +1,115 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\XApi\Lrs\Util;
use Chamilo\PluginBundle\Entity\XApi\InternalLog;
use Database;
use UserManager;
use Xabbuh\XApi\Model\Activity;
use Xabbuh\XApi\Model\Actor;
use Xabbuh\XApi\Model\Agent;
use Xabbuh\XApi\Model\IRL;
use Xabbuh\XApi\Model\Statement;
use XApiPlugin;
class InternalLogUtil
{
public static function saveStatementForInternalLog(Statement $statement)
{
if (null === $user = self::getUserFromActor($statement->getActor())) {
return;
}
$statementObject = $statement->getObject();
if (!$statementObject instanceof Activity) {
return;
}
$languageIso = api_get_language_isocode();
$statementVerbString = XApiPlugin::extractVerbInLanguage($statement->getVerb()->getDisplay(), $languageIso);
$internalLog = new InternalLog();
$internalLog
->setUser($user)
->setVerb($statementVerbString)
->setObjectId($statementObject->getId()->getValue());
if (null !== $statementId = $statement->getId()) {
$internalLog->setStatementId($statementId->getValue());
}
if (null !== $definition = $statementObject->getDefinition()) {
if (null !== $nameInLanguages = $definition->getName()) {
$internalLog->setActivityName(
XApiPlugin::extractVerbInLanguage($nameInLanguages, $languageIso)
);
}
if (null !== $descriptionInLanguage = $definition->getDescription()) {
$internalLog->setActivityDescription(
XApiPlugin::extractVerbInLanguage($descriptionInLanguage, $languageIso)
);
}
}
if (null !== $statementResult = $statement->getResult()) {
if (null !== $score = $statementResult->getScore()) {
$internalLog
->setScoreScaled(
$score->getScaled()
)
->setScoreRaw(
$score->getRaw()
)
->setScoreMin(
$score->getMin()
)
->setScoreMax(
$score->getMax()
);
}
}
if (null !== $created = $statement->getCreated()) {
$internalLog->setCreatedAt($created);
}
$em = Database::getManager();
$em->persist($internalLog);
$em->flush();
}
private static function getUserFromActor(Actor $actor): ?object
{
if (!$actor instanceof Agent) {
return null;
}
$actorIri = $actor->getInverseFunctionalIdentifier();
if (null === $actorIri) {
return null;
}
$userRepo = UserManager::getRepository();
$user = null;
if (null !== $mbox = $actorIri->getMbox()) {
if ((null !== $parts = explode(':', $mbox->getValue(), 2)) && !empty($parts[1])) {
$user = $userRepo->findOneBy(['email' => $parts[1]]);
}
} elseif (null !== $account = $actorIri->getAccount()) {
$chamiloIrl = IRL::fromString(api_get_path(WEB_PATH));
if ($account->getHomePage()->equals($chamiloIrl)) {
$user = $userRepo->findOneBy(['username' => $account->getName()]);
}
}
return $user;
}
}

View File

@@ -0,0 +1,178 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\XApi\Parser;
use Chamilo\PluginBundle\Entity\XApi\Cmi5Item;
use Chamilo\PluginBundle\Entity\XApi\ToolLaunch;
use Symfony\Component\DomCrawler\Crawler;
/**
* Class Cmi5Parser.
*
* @package Chamilo\PluginBundle\XApi\Parser
*/
class Cmi5Parser extends PackageParser
{
/**
* {@inheritDoc}
*/
public function parse(): ToolLaunch
{
$content = file_get_contents($this->filePath);
$xml = new Crawler($content);
$courseNode = $xml->filterXPath('//courseStructure/course');
$toolLaunch = new ToolLaunch();
$toolLaunch
->setTitle(
current(
$this->getLanguageStrings(
$courseNode->filterXPath('//title')
)
)
)
->setDescription(
current(
$this->getLanguageStrings(
$courseNode->filterXPath('//description')
)
)
)
->setLaunchUrl('')
->setActivityId($courseNode->attr('id'))
->setActivityType('cmi5')
->setAllowMultipleAttempts(false)
->setCreatedAt(api_get_utc_datetime(null, false, true))
->setCourse($this->course)
->setSession($this->session);
$toc = $this->generateToC($xml);
foreach ($toc as $cmi5Item) {
$toolLaunch->addItem($cmi5Item);
}
return $toolLaunch;
}
/**
* @return array
*/
private function getLanguageStrings(Crawler $node)
{
$map = [];
foreach ($node->children() as $child) {
$key = $child->attributes['lang']->value;
$value = trim($child->textContent);
$map[$key] = $value;
}
return $map;
}
/**
* @return array|\Chamilo\PluginBundle\Entity\XApi\Cmi5Item[]
*/
private function generateToC(Crawler $xml)
{
$blocksMap = [];
/** @var array|Cmi5Item[] $items */
$items = $xml
->filterXPath('//*')
->reduce(
function (Crawler $node, $i) {
return in_array($node->nodeName(), ['au', 'block']);
}
)
->each(
function (Crawler $node, $i) use (&$blocksMap) {
$attributes = ['id', 'activityType', 'launchMethod', 'moveOn', 'masteryScore'];
list($id, $activityType, $launchMethod, $moveOn, $masteryMode) = $node->extract($attributes)[0];
$item = new Cmi5Item();
$item
->setIdentifier($id)
->setType($node->nodeName())
->setTitle(
$this->getLanguageStrings(
$node->filterXPath('//title')
)
)
->setDescription(
$this->getLanguageStrings(
$node->filterXPath('//description')
)
);
if ('au' === $node->nodeName()) {
$launchParametersNode = $node->filterXPath('//launchParameters');
$entitlementKeyNode = $node->filterXPath('//entitlementKey');
$url
= $item
->setUrl(
$this->parseLaunchUrl(
trim($node->filterXPath('//url')->text())
)
)
->setActivityType($activityType ?: null)
->setLaunchMethod($launchMethod ?: null)
->setMoveOn($moveOn ?: 'NotApplicable')
->setMasteryScore((float) $masteryMode ?: null)
->setLaunchParameters(
$launchParametersNode->count() > 0 ? trim($launchParametersNode->text()) : null
)
->setEntitlementKey(
$entitlementKeyNode->count() > 0 ? trim($entitlementKeyNode->text()) : null
);
}
$parentNode = $node->parents()->first();
if ('block' === $parentNode->nodeName()) {
$blocksMap[$i] = $parentNode->attr('id');
}
return $item;
}
);
foreach ($blocksMap as $itemPos => $parentIdentifier) {
foreach ($items as $item) {
if ($parentIdentifier === $item->getIdentifier()) {
$items[$itemPos]->setParent($item);
}
}
}
return $items;
}
/**
* @param string $url
*
* @return string
*/
private function parseLaunchUrl($url)
{
$urlInfo = parse_url($url);
if (empty($urlInfo['scheme'])) {
$baseUrl = str_replace(
api_get_path(SYS_COURSE_PATH),
api_get_path(WEB_COURSE_PATH),
dirname($this->filePath)
);
return "$baseUrl/$url";
}
return $url;
}
}

View File

@@ -0,0 +1,60 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\XApi\Parser;
use Chamilo\CoreBundle\Entity\Course;
use Chamilo\CoreBundle\Entity\Session;
/**
* Class PackageParser.
*
* @package Chamilo\PluginBundle\XApi\Parser
*/
abstract class PackageParser
{
/**
* @var string
*/
protected $filePath;
/**
* @var Course
*/
protected $course;
/**
* @var Session|null
*/
protected $session;
/**
* AbstractParser constructor.
*
* @param $filePath
*/
protected function __construct($filePath, Course $course, Session $session = null)
{
$this->filePath = $filePath;
$this->course = $course;
$this->session = $session;
}
/**
* @throws \Exception
*
* @return mixed
*/
public static function create(string $packageType, string $filePath, Course $course, Session $session = null)
{
switch ($packageType) {
case 'tincan':
return new TinCanParser($filePath, $course, $session);
case 'cmi5':
return new Cmi5Parser($filePath, $course, $session);
default:
throw new \Exception('Invalid package.');
}
}
abstract public function parse(): \Chamilo\PluginBundle\Entity\XApi\ToolLaunch;
}

View File

@@ -0,0 +1,69 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\XApi\Parser;
use Chamilo\PluginBundle\Entity\XApi\ToolLaunch;
use Symfony\Component\DomCrawler\Crawler;
/**
* Class TinCanParser.
*
* @package Chamilo\PluginBundle\XApi\Parser
*/
class TinCanParser extends PackageParser
{
/**
* {@inheritDoc}
*/
public function parse(): ToolLaunch
{
$content = file_get_contents($this->filePath);
$xml = new Crawler($content);
$activityNode = $xml->filter('tincan activities activity')->first();
$nodeName = $activityNode->filter('name');
$nodeDescription = $activityNode->filter('description');
$nodeLaunch = $activityNode->filter('launch');
$toolLaunch = new ToolLaunch();
$toolLaunch
->setCourse($this->course)
->setSession($this->session)
->setCreatedAt(api_get_utc_datetime(null, false, true))
->setActivityId($activityNode->attr('id'))
->setActivityType($activityNode->attr('type'))
->setLaunchUrl($this->parseLaunchUrl($nodeLaunch));
if ($nodeName) {
$toolLaunch->setTitle($nodeName->text());
}
if ($nodeDescription) {
$toolLaunch->setDescription($nodeDescription->text() ?: null);
}
return $toolLaunch;
}
private function parseLaunchUrl(Crawler $launchNode): string
{
$launchUrl = $launchNode->text();
$urlInfo = parse_url($launchUrl);
if (empty($urlInfo['scheme'])) {
$baseUrl = str_replace(
api_get_path(SYS_COURSE_PATH),
api_get_path(WEB_COURSE_PATH),
dirname($this->filePath)
);
return "$baseUrl/$launchUrl";
}
return $launchUrl;
}
}

View File

@@ -0,0 +1,49 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\XApi\ToolExperience\Activity;
use Xabbuh\XApi\Model\Activity;
/**
* Class BaseActivity.
*
* @package Chamilo\PluginBundle\XApi\ToolExperience\Activity
*/
abstract class BaseActivity
{
/**
* @var \Chamilo\UserBundle\Entity\User
*/
protected $user;
/**
* @var \Chamilo\CoreBundle\Entity\Course|null
*/
protected $course;
/**
* @var \Chamilo\CoreBundle\Entity\Session|null
*/
protected $session;
abstract public function generate(): Activity;
protected function generateIri(string $path, string $resource, array $params = []): string
{
$cidReq = api_get_cidreq();
$url = api_get_path($path).$resource;
if ($params) {
$url .= '?'.http_build_query($params).'&';
} elseif (empty($params) && $cidReq) {
$url .= '?';
}
if ($cidReq) {
$url .= $cidReq;
}
return $url;
}
}

View File

@@ -0,0 +1,40 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\XApi\ToolExperience\Activity;
use Xabbuh\XApi\Model\Activity;
use Xabbuh\XApi\Model\Definition;
use Xabbuh\XApi\Model\IRI;
use Xabbuh\XApi\Model\LanguageMap;
/**
* Class Course.
*
* @package Chamilo\PluginBundle\XApi\ToolExperience\Activity
*/
class Course extends BaseActivity
{
public function generate(): Activity
{
$course = api_get_course_entity();
$session = api_get_session_entity();
$languageIso = api_get_language_isocode($course->getCourseLanguage());
$courseUrl = api_get_course_url(
$course->getCode(),
$session ? $session->getId() : 0
);
return new Activity(
IRI::fromString($courseUrl),
new Definition(
LanguageMap::create([$languageIso => $course->getTitle()]),
null,
IRI::fromString('http://id.tincanapi.com/activitytype/lms/course')
)
);
}
}

View File

@@ -0,0 +1,53 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\XApi\ToolExperience\Activity;
use Chamilo\CourseBundle\Entity\CLp;
use Xabbuh\XApi\Model\Activity;
use Xabbuh\XApi\Model\Definition;
use Xabbuh\XApi\Model\IRI;
use Xabbuh\XApi\Model\LanguageMap;
/**
* Class LearningPath.
*
* @package Chamilo\PluginBundle\XApi\ToolExperience\Activity
*/
class LearningPath extends BaseActivity
{
/**
* @var \Chamilo\CourseBundle\Entity\CLp
*/
private $lp;
public function __construct(CLp $lp)
{
$this->lp = $lp;
}
public function generate(): Activity
{
$lanIso = api_get_language_isocode();
$iri = $this->generateIri(
WEB_CODE_PATH,
'lp/lp_controller.php',
[
'action' => 'view',
'lp_id' => $this->lp->getId(),
'isStudentView' => 'true',
]
);
return new Activity(
IRI::fromString($iri),
new Definition(
LanguageMap::create([$lanIso => $this->lp->getName()]),
null,
IRI::fromString('http://adlnet.gov/expapi/activities/lesson')
)
);
}
}

View File

@@ -0,0 +1,54 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\XApi\ToolExperience\Activity;
use Chamilo\CourseBundle\Entity\CLpItem;
use Xabbuh\XApi\Model\Activity;
use Xabbuh\XApi\Model\Definition;
use Xabbuh\XApi\Model\IRI;
use Xabbuh\XApi\Model\LanguageMap;
/**
* Class LearningPathItem.
*
* @package Chamilo\PluginBundle\XApi\ToolExperience\Activity
*/
class LearningPathItem extends BaseActivity
{
/**
* @var \Chamilo\CourseBundle\Entity\CLpItem
*/
private $lpItem;
public function __construct(CLpItem $lpItem)
{
$this->lpItem = $lpItem;
}
public function generate(): Activity
{
$langIso = api_get_language_isocode();
$iri = $this->generateIri(
WEB_CODE_PATH,
'lp/lp_controller.php',
[
'action' => 'view',
'lp_id' => $this->lpItem->getLpId(),
'isStudentView' => 'true',
'lp_item' => $this->lpItem->getId(),
]
);
return new Activity(
IRI::fromString($iri),
new Definition(
LanguageMap::create([$langIso => $this->lpItem->getTitle()]),
null,
IRI::fromString('http://id.tincanapi.com/activitytype/resource')
)
);
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace Chamilo\PluginBundle\XApi\ToolExperience\Activity;
use Chamilo\UserBundle\Entity\User;
use Xabbuh\XApi\Model\Activity;
use Xabbuh\XApi\Model\Definition;
use Xabbuh\XApi\Model\IRI;
use Xabbuh\XApi\Model\LanguageMap;
class Portfolio extends BaseActivity
{
/** @var User */
private $owner;
public function __construct(User $owner)
{
$this->owner = $owner;
}
public function generate(): Activity
{
$langIso = api_get_language_isocode();
$iri = $this->generateIri(
WEB_CODE_PATH,
'portfolio/index.php',
[
'action' => 'list',
'user' => $this->owner->getId(),
]
);
return new Activity(
IRI::fromString($iri),
new Definition(
LanguageMap::create(
[
$langIso => sprintf(
get_lang('XUserPortfolioItems'),
$this->owner->getCompleteNameWithUsername()
),
]
),
null,
IRI::fromString('http://id.tincanapi.com/activitytype/collection-simple')
)
);
}
}

View File

@@ -0,0 +1,58 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\XApi\ToolExperience\Activity;
use Chamilo\CoreBundle\Entity\PortfolioCategory as PortfolioCategoryEntity;
use Xabbuh\XApi\Model\Activity;
use Xabbuh\XApi\Model\Definition;
use Xabbuh\XApi\Model\IRI;
use Xabbuh\XApi\Model\LanguageMap;
/**
* Class PortfolioCategory.
*
* @package Chamilo\PluginBundle\XApi\ToolExperience\Activity
*/
class PortfolioCategory extends BaseActivity
{
/**
* @var \Chamilo\CoreBundle\Entity\PortfolioCategory
*/
private $category;
public function __construct(PortfolioCategoryEntity $category)
{
$this->category = $category;
}
public function generate(): Activity
{
$iri = $this->generateIri(
WEB_PATH,
'xapi/portfolio/',
[
'user' => $this->category->getUser()->getId(),
'category' => $this->category->getId(),
]
);
$langIso = api_get_language_isocode();
$categoryDescription = $this->category->getDescription();
$definitionDescription = $categoryDescription
? LanguageMap::create([$langIso => $categoryDescription])
: null;
return new Activity(
IRI::fromString($iri),
new Definition(
LanguageMap::create([$langIso => $this->category->getTitle()]),
$definitionDescription,
IRI::fromString('http://id.tincanapi.com/activitytype/category')
)
);
}
}

View File

@@ -0,0 +1,50 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\XApi\ToolExperience\Activity;
use Chamilo\CoreBundle\Entity\PortfolioComment as PortfolioCommentEntity;
use Xabbuh\XApi\Model\Activity;
use Xabbuh\XApi\Model\Definition;
use Xabbuh\XApi\Model\IRI;
/**
* Class PortfolioComment.
*
* @package Chamilo\PluginBundle\XApi\ToolExperience\Activity
*/
class PortfolioComment extends BaseActivity
{
/**
* @var \Chamilo\CoreBundle\Entity\PortfolioComment
*/
private $comment;
public function __construct(PortfolioCommentEntity $comment)
{
$this->comment = $comment;
}
public function generate(): Activity
{
$iri = $this->generateIri(
WEB_CODE_PATH,
'portfolio/index.php',
[
'action' => 'view',
'id' => $this->comment->getItem()->getId(),
'comment' => $this->comment->getId(),
]
);
return new Activity(
IRI::fromString($iri),
new Definition(
null,
null,
IRI::fromString('http://activitystrea.ms/schema/1.0/comment')
)
);
}
}

View File

@@ -0,0 +1,49 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\XApi\ToolExperience\Activity;
use Chamilo\CoreBundle\Entity\Portfolio;
use Xabbuh\XApi\Model\Activity;
use Xabbuh\XApi\Model\Definition;
use Xabbuh\XApi\Model\IRI;
use Xabbuh\XApi\Model\LanguageMap;
/**
* Class PortfolioItem.
*
* @package Chamilo\PluginBundle\XApi\ToolExperience\Activity
*/
class PortfolioItem extends BaseActivity
{
/**
* @var \Chamilo\CoreBundle\Entity\Portfolio
*/
private $item;
public function __construct(Portfolio $item)
{
$this->item = $item;
}
public function generate(): Activity
{
$langIso = api_get_language_isocode();
$iri = $this->generateIri(
WEB_CODE_PATH,
'portfolio/index.php',
['action' => 'view', 'id' => $this->item->getId()]
);
return new Activity(
IRI::fromString($iri),
new Definition(
LanguageMap::create([$langIso => $this->item->getTitle()]),
null,
IRI::fromString('http://activitystrea.ms/schema/1.0/article')
)
);
}
}

View File

@@ -0,0 +1,57 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\XApi\ToolExperience\Activity;
use Chamilo\CourseBundle\Entity\CQuiz;
use Xabbuh\XApi\Model\Activity;
use Xabbuh\XApi\Model\Definition;
use Xabbuh\XApi\Model\IRI;
use Xabbuh\XApi\Model\LanguageMap;
/**
* Class Quiz.
*
* @package Chamilo\PluginBundle\XApi\ToolExperience\Activity
*/
class Quiz extends BaseActivity
{
/**
* @var \Chamilo\CourseBundle\Entity\CQuiz
*/
private $quiz;
public function __construct(CQuiz $quiz)
{
$this->quiz = $quiz;
}
public function generate(): Activity
{
$langIso = api_get_language_isocode();
$iri = $this->generateIri(
WEB_CODE_PATH,
'exercise/overview.php',
['exerciseId' => $this->quiz->getId()]
);
$definitionDescription = null;
if ($this->quiz->getDescription()) {
$definitionDescription = LanguageMap::create(
[$langIso => $this->quiz->getDescription()]
);
}
return new Activity(
IRI::fromString($iri),
new Definition(
LanguageMap::create([$langIso => $this->quiz->getTitle()]),
$definitionDescription,
IRI::fromString('http://adlnet.gov/expapi/activities/assessment')
)
);
}
}

View File

@@ -0,0 +1,170 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\XApi\ToolExperience\Activity;
use Answer;
use Chamilo\CourseBundle\Entity\CQuizQuestion;
use Xabbuh\XApi\Model\Activity;
use Xabbuh\XApi\Model\Interaction\ChoiceInteractionDefinition;
use Xabbuh\XApi\Model\Interaction\InteractionComponent;
use Xabbuh\XApi\Model\Interaction\LongFillInInteractionDefinition;
use Xabbuh\XApi\Model\Interaction\MatchingInteractionDefinition;
use Xabbuh\XApi\Model\Interaction\OtherInteractionDefinition;
use Xabbuh\XApi\Model\Interaction\SequencingInteractionDefinition;
use Xabbuh\XApi\Model\IRI;
use Xabbuh\XApi\Model\LanguageMap;
/**
* Class QuizQuestion.
*
* @package Chamilo\PluginBundle\XApi\ToolExperience\Activity
*/
class QuizQuestion extends BaseActivity
{
private $question;
public function __construct(CQuizQuestion $question)
{
$this->question = $question;
}
public function generate(): Activity
{
$iri = $this->generateIri(
WEB_CODE_PATH,
'xapi/quiz/',
['question' => $this->question->getId()]
);
return new Activity(
IRI::fromString($iri),
$this->generateActivityDefinitionFromQuestionType()
);
}
/**
* @return \Xabbuh\XApi\Model\Interaction\InteractionDefinition
*/
private function generateActivityDefinitionFromQuestionType()
{
$languageIso = api_get_language_isocode();
$courseId = api_get_course_int_id();
$questionTitle = strip_tags($this->question->getQuestion());
$questionTitle = trim($questionTitle);
$questionDescription = strip_tags($this->question->getDescription());
$questionDescription = trim($questionDescription);
$titleMap = LanguageMap::create([$languageIso => $questionTitle]);
$descriptionMap = $questionDescription ? LanguageMap::create([$languageIso => $questionDescription]) : null;
$objAnswer = new Answer($this->question->getId(), $courseId);
$objAnswer->read();
$type = IRI::fromString('http://adlnet.gov/expapi/activities/question');
switch ($this->question->getType()) {
case MULTIPLE_ANSWER:
case UNIQUE_ANSWER:
case UNIQUE_ANSWER_IMAGE:
case READING_COMPREHENSION:
$choices = [];
$correctResponsesPattern = [];
for ($i = 1; $i <= $objAnswer->nbrAnswers; $i++) {
$choices[] = new InteractionComponent(
$objAnswer->iid[$i],
LanguageMap::create([$languageIso => $objAnswer->selectAnswer($i)])
);
if ($objAnswer->isCorrect($i)) {
$correctResponsesPattern[] = $objAnswer->iid[$i];
}
}
return new ChoiceInteractionDefinition(
$titleMap,
$descriptionMap,
$type,
null,
null,
[implode('[,]', $correctResponsesPattern)],
$choices
);
case DRAGGABLE:
$choices = [];
for ($i = 1; $i <= $objAnswer->nbrAnswers; $i++) {
if ((int) $objAnswer->correct[$i] > 0) {
$choices[] = new InteractionComponent(
$objAnswer->correct[$i],
LanguageMap::create([$languageIso => $objAnswer->answer[$i]])
);
}
}
$correctResponsesPattern = array_slice($objAnswer->autoId, 0, $objAnswer->nbrAnswers / 2);
return new SequencingInteractionDefinition(
$titleMap,
$descriptionMap,
$type,
null,
null,
[implode('[,]', $correctResponsesPattern)],
$choices
);
case MATCHING:
case MATCHING_DRAGGABLE:
/** @var array|InteractionComponent[] $source */
$source = [];
/** @var array|InteractionComponent[] $source */
$target = [];
$correctResponsesPattern = [];
for ($i = 1; $i <= $objAnswer->nbrAnswers; $i++) {
$interactionComponent = new InteractionComponent(
$objAnswer->selectAutoId($i),
LanguageMap::create([$languageIso => $objAnswer->selectAnswer($i)])
);
if ((int) $objAnswer->correct[$i] > 0) {
$source[] = $interactionComponent;
$correctResponsesPattern[] = $objAnswer->selectAutoId($i).'[.]'.$objAnswer->correct[$i];
} else {
$target[] = $interactionComponent;
}
}
return new MatchingInteractionDefinition(
$titleMap,
$descriptionMap,
$type,
null,
null,
[implode('[,]', $correctResponsesPattern)],
$source,
$target
);
case FREE_ANSWER:
return new LongFillInInteractionDefinition($titleMap, $descriptionMap, $type);
case FILL_IN_BLANKS:
case HOT_SPOT:
case HOT_SPOT_DELINEATION:
case MULTIPLE_ANSWER_COMBINATION:
case UNIQUE_ANSWER_NO_OPTION:
case MULTIPLE_ANSWER_TRUE_FALSE:
case MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY:
case MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE:
case GLOBAL_MULTIPLE_ANSWER:
case CALCULATED_ANSWER:
case ANNOTATION:
case ORAL_EXPRESSION:
default:
return new OtherInteractionDefinition($titleMap, $descriptionMap, $type);
}
}
}

View File

@@ -0,0 +1,33 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\XApi\ToolExperience\Activity;
use Xabbuh\XApi\Model\Activity;
use Xabbuh\XApi\Model\Definition;
use Xabbuh\XApi\Model\IRI;
use Xabbuh\XApi\Model\LanguageMap;
/**
* Class Site.
*
* @package Chamilo\PluginBundle\XApi\ToolExperience\Activity
*/
class Site extends BaseActivity
{
public function generate(): Activity
{
$platformLanguageIso = api_get_language_isocode(
api_get_setting('platformLanguage')
);
$platform = api_get_setting('Institution').' - '.api_get_setting('siteName');
return new Activity(
IRI::fromString('http://id.tincanapi.com/activitytype/lms'),
new Definition(
LanguageMap::create([$platformLanguageIso => $platform])
)
);
}
}

View File

@@ -0,0 +1,17 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\XApi\ToolExperience\Actor;
use Xabbuh\XApi\Model\Agent;
/**
* Class BaseActor.
*
* @package Chamilo\PluginBundle\XApi\ToolExperience\Actor
*/
abstract class BaseActor
{
abstract public function generate(): Agent;
}

View File

@@ -0,0 +1,35 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\XApi\ToolExperience\Actor;
use Chamilo\UserBundle\Entity\User as UserEntity;
use Xabbuh\XApi\Model\Agent;
use Xabbuh\XApi\Model\InverseFunctionalIdentifier;
use Xabbuh\XApi\Model\IRI;
/**
* Class User.
*
* @package Chamilo\PluginBundle\XApi\ToolExperience\Actor
*/
class User extends BaseActor
{
private $user;
public function __construct(UserEntity $user)
{
$this->user = $user;
}
public function generate(): Agent
{
return new Agent(
InverseFunctionalIdentifier::withMbox(
IRI::fromString('mailto:'.$this->user->getEmail())
),
$this->user->getCompleteName()
);
}
}

Some files were not shown because too many files have changed in this diff Show More