commit
77064d772e
@ -0,0 +1 @@ |
||||
AcceptPathInfo On |
@ -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; |
||||
``` |
@ -0,0 +1,210 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/* For licensing terms, see /license.txt */ |
||||
|
||||
use Chamilo\CoreBundle\Entity\XApiLrsAuth; |
||||
use Symfony\Component\HttpFoundation\Request; |
||||
|
||||
$cidReset = true; |
||||
|
||||
require_once __DIR__.'/../../main/inc/global.inc.php'; |
||||
|
||||
api_protect_admin_script(); |
||||
|
||||
$request = Request::createFromGlobals(); |
||||
$plugin = XApiPlugin::create(); |
||||
$em = Database::getManager(); |
||||
|
||||
$pageBaseUrl = api_get_self(); |
||||
$pageActions = ''; |
||||
$pageContent = ''; |
||||
|
||||
/** |
||||
* @return FormValidator |
||||
* |
||||
* @throws Exception |
||||
*/ |
||||
function createForm(?XApiLrsAuth $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 XApiLrsAuth(); |
||||
$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(XApiLrsAuth::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(XApiLrsAuth::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(XApiLrsAuth::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(); |
@ -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; |
||||
} |
||||
} |
@ -0,0 +1,2 @@ |
||||
$(function () { |
||||
}); |
@ -0,0 +1,192 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/* For licensing terms, see /license.txt */ |
||||
|
||||
use Chamilo\CoreBundle\Entity\XApiCmi5Item; |
||||
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(XApiCmi5Item::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"); |
@ -0,0 +1,24 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/* 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(); |
@ -0,0 +1,86 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/* For licensing terms, see /license.txt */ |
||||
|
||||
use Chamilo\CoreBundle\Entity\XApiCmi5Item; |
||||
use Chamilo\CoreBundle\Entity\XApiToolLaunch; |
||||
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( |
||||
XApiToolLaunch::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(XApiCmi5Item::class); |
||||
|
||||
$query = $itemsRepo->createQueryBuilder('item'); |
||||
$query |
||||
->where($query->expr()->eq('item.tool', ':tool')) |
||||
->setParameter('tool', $toolLaunch->getId()) |
||||
; |
||||
|
||||
$tocHtml = $itemsRepo->buildTree( |
||||
$query->getQuery()->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(); |
@ -0,0 +1,82 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/* For licensing terms, see /license.txt */ |
||||
|
||||
use Chamilo\CoreBundle\Entity\XApiSharedStatement; |
||||
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 !== 'cli') { |
||||
exit; |
||||
} |
||||
|
||||
echo 'XAPI: Cron to send statements.'.\PHP_EOL; |
||||
|
||||
$em = Database::getManager(); |
||||
$serializer = Serializer::createSerializer(); |
||||
$statementSerializer = new StatementSerializer($serializer); |
||||
|
||||
$notSentSharedStatements = $em |
||||
->getRepository(XApiSharedStatement::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(); |
||||
|
||||
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; |
||||
} |
@ -0,0 +1,58 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/* 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'; |
@ -0,0 +1,51 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/* 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'; |
@ -0,0 +1,58 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/* 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'; |
@ -0,0 +1,14 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/* 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(); |
@ -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. |
@ -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" |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,211 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/* |
||||
* 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 DateTime; |
||||
use Exception; |
||||
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; |
||||
|
||||
use const FILTER_VALIDATE_BOOLEAN; |
||||
|
||||
/** |
||||
* @author Jérôme Parmentier <jerome.parmentier@acensi.fr> |
||||
*/ |
||||
class StatementGetController |
||||
{ |
||||
protected static array $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, |
||||
]; |
||||
|
||||
public function __construct( |
||||
protected readonly StatementRepositoryInterface $repository, |
||||
protected readonly StatementSerializerInterface $statementSerializer, |
||||
protected readonly StatementResultSerializerInterface $statementResultSerializer, |
||||
protected readonly StatementsFilterFactory $statementsFilterFactory |
||||
) {} |
||||
|
||||
/** |
||||
* @return Response |
||||
* |
||||
* @throws BadRequestHttpException if the query parameters does not comply with xAPI specification |
||||
*/ |
||||
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): void |
||||
{ |
||||
$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) |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,29 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/* |
||||
* 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 |
||||
{ |
||||
/** |
||||
* @return Response |
||||
* |
||||
* @throws BadRequestHttpException if the query parameters does not comply with xAPI specification |
||||
*/ |
||||
public function getStatement(Request $request) |
||||
{ |
||||
return parent::getStatement($request)->setContent(''); |
||||
} |
||||
} |
@ -0,0 +1,69 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/* |
||||
* 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); |
||||
} |
||||
} |
@ -0,0 +1,68 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/* |
||||
* 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 InvalidArgumentException; |
||||
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); |
||||
} |
||||
} |
@ -0,0 +1,39 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
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; |
||||
} |
||||
} |
@ -0,0 +1,66 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/* |
||||
* 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): void |
||||
{ |
||||
$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'; |
||||
} |
||||
} |
@ -0,0 +1,75 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/* |
||||
* 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): void |
||||
{ |
||||
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); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,17 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
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): void {} |
||||
} |
@ -0,0 +1,43 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
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): void |
||||
{ |
||||
$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); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,68 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/* |
||||
* 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): void |
||||
{ |
||||
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): void |
||||
{ |
||||
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'); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,92 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/* |
||||
* 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 DateTime; |
||||
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; |
||||
|
||||
use const FILTER_VALIDATE_BOOLEAN; |
||||
|
||||
/** |
||||
* @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->cursor($parameters->getInt('cursor')); |
||||
$filter->limit($parameters->getInt('limit')); |
||||
|
||||
return $filter; |
||||
} |
||||
} |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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:postStatements</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> |
@ -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> |
@ -0,0 +1,70 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/* |
||||
* 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 LogicException; |
||||
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; |
||||
} |
||||
|
||||
public function prepare(Request $request): void |
||||
{ |
||||
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()); |
||||
} |
||||
|
||||
/** |
||||
* @throws LogicException |
||||
*/ |
||||
public function sendContent(): void |
||||
{ |
||||
throw new LogicException('An AttachmentResponse is only meant to be part of a multipart Response.'); |
||||
} |
||||
|
||||
/** |
||||
* @throws LogicException when the content is not null |
||||
*/ |
||||
public function setContent($content): void |
||||
{ |
||||
if (null !== $content) { |
||||
throw new LogicException('The content cannot be set on an AttachmentResponse instance.'); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @return string|null |
||||
*/ |
||||
public function getContent() |
||||
{ |
||||
return $this->attachment->getContent(); |
||||
} |
||||
} |
@ -0,0 +1,129 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/* |
||||
* 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 LogicException; |
||||
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 (null !== $part->getContent()) { |
||||
$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; |
||||
} |
||||
|
||||
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); |
||||
} |
||||
|
||||
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; |
||||
} |
||||
|
||||
/** |
||||
* @throws LogicException when the content is not null |
||||
*/ |
||||
public function setContent($content): void |
||||
{ |
||||
if (null !== $content) { |
||||
throw new LogicException('The content cannot be set on a MultipartResponse instance.'); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @return false |
||||
*/ |
||||
public function getContent() |
||||
{ |
||||
return false; |
||||
} |
||||
} |
@ -0,0 +1,28 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/* |
||||
* 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(); |
||||
} |
||||
} |
@ -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. |
@ -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" |
||||
} |
||||
} |
||||
} |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -0,0 +1,70 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/* |
||||
* 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 |
||||
{ |
||||
public function findStatement(array $criteria) |
||||
{ |
||||
return parent::findOneBy($criteria); |
||||
} |
||||
|
||||
public function findStatements(array $criteria) |
||||
{ |
||||
if (!empty($criteria['registration'])) { |
||||
$contexts = $this->_em->getRepository(Context::class)->findBy([ |
||||
'registration' => $criteria['registration'], |
||||
]); |
||||
|
||||
$criteria['context'] = $contexts; |
||||
} |
||||
|
||||
if (!empty($criteria['verb'])) { |
||||
$verbs = $this->_em->getRepository(Verb::class)->findBy(['id' => $criteria['verb']]); |
||||
|
||||
$criteria['verb'] = $verbs; |
||||
} |
||||
|
||||
unset( |
||||
$criteria['registration'], |
||||
$criteria['related_activities'], |
||||
$criteria['related_agents'], |
||||
$criteria['ascending'], |
||||
); |
||||
|
||||
return parent::findBy( |
||||
$criteria, |
||||
['created' => 'ASC'], |
||||
$criteria['limit'] ?? null, |
||||
$criteria['cursor'] ?? null |
||||
); |
||||
} |
||||
|
||||
public function storeStatement(Statement $mappedStatement, $flush = true): void |
||||
{ |
||||
$this->_em->persist($mappedStatement); |
||||
|
||||
if ($flush) { |
||||
$this->_em->flush(); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,7 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/* For licensing terms, see /license.txt */ |
||||
|
||||
$plugin_info = XApiPlugin::create()->get_info(); |
@ -0,0 +1,64 @@ |
||||
<?php |
||||
|
||||
/* For licensing terms, see /license.txt */ |
||||
|
||||
use Chamilo\CoreBundle\Entity\XApiSharedStatement; |
||||
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 |
||||
*/ |
||||
protected function saveSharedStatement(Statement $statement): XApiSharedStatement |
||||
{ |
||||
$statementSerialized = $this->serializeStatement($statement); |
||||
|
||||
$sharedStmt = new XApiSharedStatement( |
||||
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); |
||||
} |
||||
} |
@ -0,0 +1,30 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/* For licensing terms, see /license.txt */ |
||||
|
||||
class XApiCreateCourseHookObserver extends HookObserver implements HookCreateCourseObserverInterface |
||||
{ |
||||
protected function __construct() |
||||
{ |
||||
parent::__construct( |
||||
'plugin/xapi/src/XApiPlugin.php', |
||||
'xapi' |
||||
); |
||||
} |
||||
|
||||
public function hookCreateCourse(HookCreateCourseEventInterface $hook): void |
||||
{ |
||||
$data = $hook->getEventData(); |
||||
|
||||
$type = $data['type']; |
||||
$courseInfo = $data['course_info']; |
||||
|
||||
$plugin = XApiPlugin::create(); |
||||
|
||||
if (HOOK_EVENT_TYPE_POST == $type) { |
||||
$plugin->addCourseToolForTinCan($courseInfo['real_id']); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,25 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/* For licensing terms, see /license.txt */ |
||||
|
||||
use Chamilo\PluginBundle\XApi\ToolExperience\Statement\LearningPathCompleted; |
||||
|
||||
class XApiLearningPathEndHookObserver extends XApiActivityHookObserver implements HookLearningPathEndObserverInterface |
||||
{ |
||||
public function notifyLearningPathEnd(HookLearningPathEndEventInterface $event): void |
||||
{ |
||||
$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); |
||||
} |
||||
} |
@ -0,0 +1,31 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/* For licensing terms, see /license.txt */ |
||||
|
||||
use Chamilo\PluginBundle\XApi\ToolExperience\Statement\LearningPathItemViewed; |
||||
|
||||
class XApiLearningPathItemViewedHookObserver extends XApiActivityHookObserver implements HookLearningPathItemViewedObserverInterface |
||||
{ |
||||
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); |
||||
} |
||||
} |
@ -0,0 +1,21 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/* 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): void |
||||
{ |
||||
/** @var PortfolioComment $comment */ |
||||
$comment = $hookEvent->getEventData()['comment']; |
||||
|
||||
$statement = (new PortfolioCommentEdited($comment))->generate(); |
||||
|
||||
$this->saveSharedStatement($statement); |
||||
} |
||||
} |
@ -0,0 +1,21 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/* 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): void |
||||
{ |
||||
/** @var PortfolioComment $comment */ |
||||
$comment = $hookEvent->getEventData()['comment']; |
||||
|
||||
$statement = (new PortfolioCommentScored($comment))->generate(); |
||||
|
||||
$this->saveSharedStatement($statement); |
||||
} |
||||
} |
@ -0,0 +1,19 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
use Chamilo\PluginBundle\XApi\ToolExperience\Statement\PortfolioDownloaded; |
||||
use Chamilo\UserBundle\Entity\User; |
||||
|
||||
class XApiPortfolioDownloadedHookObserver extends XApiActivityHookObserver implements HookPortfolioDownloadedObserverInterface |
||||
{ |
||||
public function hookPortfolioDownloaded(HookPortfolioDownloadedEventInterface $hookEvent): void |
||||
{ |
||||
/** @var User $owner */ |
||||
$owner = $hookEvent->getEventData()['owner']; |
||||
|
||||
$statement = (new PortfolioDownloaded($owner))->generate(); |
||||
|
||||
$this->saveSharedStatement($statement); |
||||
} |
||||
} |
@ -0,0 +1,21 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/* For licensing terms, see /license.txt */ |
||||
|
||||
use Chamilo\PluginBundle\XApi\ToolExperience\Statement\PortfolioItemShared; |
||||
|
||||
class XApiPortfolioItemAddedHookObserver extends XApiActivityHookObserver implements HookPortfolioItemAddedObserverInterface |
||||
{ |
||||
public function hookItemAdded(HookPortfolioItemAddedEventInterface $hookEvent): void |
||||
{ |
||||
$item = $hookEvent->getEventData()['portfolio']; |
||||
|
||||
$portfolioItemShared = new PortfolioItemShared($item); |
||||
|
||||
$statement = $portfolioItemShared->generate(); |
||||
|
||||
$this->saveSharedStatement($statement); |
||||
} |
||||
} |
@ -0,0 +1,21 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/* For licensing terms, see /license.txt */ |
||||
|
||||
use Chamilo\PluginBundle\XApi\ToolExperience\Statement\PortfolioItemCommented; |
||||
|
||||
class XApiPortfolioItemCommentedHookObserver extends XApiActivityHookObserver implements HookPortfolioItemCommentedObserverInterface |
||||
{ |
||||
public function hookItemCommented(HookPortfolioItemCommentedEventInterface $hookEvent): void |
||||
{ |
||||
$comment = $hookEvent->getEventData()['comment']; |
||||
|
||||
$portfolioItemCommented = new PortfolioItemCommented($comment); |
||||
|
||||
$statement = $portfolioItemCommented->generate(); |
||||
|
||||
$this->saveSharedStatement($statement); |
||||
} |
||||
} |
@ -0,0 +1,21 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/* 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): void |
||||
{ |
||||
/** @var Portfolio $item */ |
||||
$item = $hookEvent->getEventData()['item']; |
||||
|
||||
$statement = (new PortfolioItemEdited($item))->generate(); |
||||
|
||||
$this->saveSharedStatement($statement); |
||||
} |
||||
} |
@ -0,0 +1,24 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
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): void |
||||
{ |
||||
/** @var Portfolio $item */ |
||||
$item = $hookEvent->getEventData()['item']; |
||||
|
||||
$statement = (new PortfolioItemHighlighted($item))->generate(); |
||||
|
||||
$this->saveSharedStatement($statement); |
||||
} |
||||
} |
@ -0,0 +1,21 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/* 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): void |
||||
{ |
||||
/** @var Portfolio $item */ |
||||
$item = $hookEvent->getEventData()['item']; |
||||
|
||||
$statement = (new PortfolioItemScored($item))->generate(); |
||||
|
||||
$this->saveSharedStatement($statement); |
||||
} |
||||
} |
@ -0,0 +1,27 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/* 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): void |
||||
{ |
||||
/** @var Portfolio $item */ |
||||
$item = $hookEvent->getEventData()['portfolio']; |
||||
|
||||
$statement = (new PortfolioItemViewed($item))->generate(); |
||||
|
||||
$this->saveSharedStatement($statement); |
||||
} |
||||
} |
@ -0,0 +1,25 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/* For licensing terms, see /license.txt */ |
||||
|
||||
use Chamilo\PluginBundle\XApi\ToolExperience\Statement\QuizCompleted; |
||||
|
||||
class XApiQuizEndHookObserver extends XApiActivityHookObserver implements HookQuizEndObserverInterface |
||||
{ |
||||
public function hookQuizEnd(HookQuizEndEventInterface $hookEvent): void |
||||
{ |
||||
$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); |
||||
} |
||||
} |
@ -0,0 +1,38 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/* 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 XApiQuizQuestionAnsweredHookObserver extends XApiActivityHookObserver implements HookQuizQuestionAnsweredObserverInterface |
||||
{ |
||||
public function hookQuizQuestionAnswered(HookQuizQuestionAnsweredEventInterface $event): void |
||||
{ |
||||
$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); |
||||
} |
||||
} |
@ -0,0 +1,68 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/* For licensing terms, see /license.txt */ |
||||
|
||||
namespace Chamilo\PluginBundle\XApi\Importer; |
||||
|
||||
use Chamilo\CoreBundle\Entity\Course; |
||||
use Exception; |
||||
|
||||
/** |
||||
* Class AbstractImporter. |
||||
*/ |
||||
abstract class PackageImporter |
||||
{ |
||||
/** |
||||
* @var Course |
||||
*/ |
||||
protected $course; |
||||
|
||||
/** |
||||
* @var string |
||||
*/ |
||||
protected $courseDirectoryPath; |
||||
|
||||
/** |
||||
* @var array |
||||
*/ |
||||
protected $packageFileInfo; |
||||
|
||||
/** |
||||
* @var string |
||||
*/ |
||||
protected $packageType; |
||||
|
||||
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); |
||||
} |
||||
|
||||
/** |
||||
* @return mixed |
||||
* |
||||
* @throws Exception |
||||
*/ |
||||
abstract public function import(): string; |
||||
|
||||
public function getPackageType(): string |
||||
{ |
||||
return $this->packageType; |
||||
} |
||||
} |
@ -0,0 +1,26 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/* For licensing terms, see /license.txt */ |
||||
|
||||
namespace Chamilo\PluginBundle\XApi\Importer; |
||||
|
||||
use Exception; |
||||
|
||||
/** |
||||
* Class XmlImporter. |
||||
*/ |
||||
class XmlPackageImporter extends PackageImporter |
||||
{ |
||||
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']; |
||||
} |
||||
} |
@ -0,0 +1,85 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/* For licensing terms, see /license.txt */ |
||||
|
||||
namespace Chamilo\PluginBundle\XApi\Importer; |
||||
|
||||
use DocumentManager; |
||||
use Exception; |
||||
use PclZip; |
||||
use Symfony\Component\Filesystem\Filesystem; |
||||
|
||||
/** |
||||
* Class ZipImporter. |
||||
*/ |
||||
class ZipPackageImporter extends PackageImporter |
||||
{ |
||||
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): void |
||||
{ |
||||
$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; |
||||
} |
||||
} |
@ -0,0 +1,30 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/* For licensing terms, see /license.txt */ |
||||
|
||||
namespace Chamilo\PluginBundle\XApi\Lrs; |
||||
|
||||
use Symfony\Component\HttpFoundation\JsonResponse; |
||||
use Symfony\Component\HttpFoundation\Response; |
||||
|
||||
/** |
||||
* Class AboutController. |
||||
*/ |
||||
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); |
||||
} |
||||
} |
@ -0,0 +1,80 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/* For licensing terms, see /license.txt */ |
||||
|
||||
namespace Chamilo\PluginBundle\XApi\Lrs; |
||||
|
||||
use Chamilo\CoreBundle\Entity\XApiActivityProfile; |
||||
use Database; |
||||
use Symfony\Component\HttpFoundation\Response; |
||||
|
||||
/** |
||||
* Class ActivitiesProfileController. |
||||
*/ |
||||
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(XApiActivityProfile::class); |
||||
|
||||
/** @var XApiActivityProfile $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(XApiActivityProfile::class); |
||||
|
||||
/** @var XApiActivityProfile $activityProfile */ |
||||
$activityProfile = $profileRepo->findOneBy( |
||||
[ |
||||
'profileId' => $profileId, |
||||
'activityId' => $activityId, |
||||
] |
||||
); |
||||
|
||||
if (empty($activityProfile)) { |
||||
$activityProfile = new XApiActivityProfile(); |
||||
$activityProfile |
||||
->setProfileId($profileId) |
||||
->setActivityId($activityId) |
||||
; |
||||
} |
||||
|
||||
$activityProfile->setDocumentData(json_decode($documentData, true)); |
||||
|
||||
$em->persist($activityProfile); |
||||
$em->flush(); |
||||
|
||||
return Response::create(null, Response::HTTP_NO_CONTENT); |
||||
} |
||||
} |
@ -0,0 +1,121 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
/* For licensing terms, see /license.txt */ |
||||
|
||||
namespace Chamilo\PluginBundle\XApi\Lrs; |
||||
|
||||
use Chamilo\CoreBundle\Entity\XApiActivityState; |
||||
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. |
||||
*/ |
||||
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 XApiActivityState(); |
||||
$state |
||||
->setActivityId($activityId) |
||||
->setAgent(json_decode($agent, true)) |
||||
->setStateId($stateId) |
||||
; |
||||
} else { |
||||
$state = $em->find(XApiActivityState::class, $state['id']); |
||||
} |
||||
|
||||
$state->setDocumentData(json_decode($documentData, true)); |
||||
|
||||
$em->persist($state); |
||||
$em->flush(); |
||||
|
||||
return Response::create('', Response::HTTP_NO_CONTENT); |
||||
} |
||||
} |
@ -0,0 +1,25 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/* For licensing terms, see /license.txt */ |
||||
|
||||
namespace Chamilo\PluginBundle\XApi\Lrs; |
||||
|
||||
use Symfony\Component\HttpFoundation\Request; |
||||
|
||||
/** |
||||
* Class BaseController. |
||||
*/ |
||||
abstract class BaseController |
||||
{ |
||||
/** |
||||
* @var Request |
||||
*/ |
||||
protected $httpRequest; |
||||
|
||||
public function __construct(Request $httpRequest) |
||||
{ |
||||
$this->httpRequest = $httpRequest; |
||||
} |
||||
} |
@ -0,0 +1,218 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
/* For licensing terms, see /license.txt */ |
||||
|
||||
namespace Chamilo\PluginBundle\XApi\Lrs; |
||||
|
||||
use Chamilo\CoreBundle\Entity\XApiLrsAuth; |
||||
use Database; |
||||
use Exception; |
||||
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. |
||||
*/ |
||||
class LrsRequest |
||||
{ |
||||
/** |
||||
* @var HttpRequest |
||||
*/ |
||||
private $request; |
||||
|
||||
public function __construct() |
||||
{ |
||||
$this->request = HttpRequest::createFromGlobals(); |
||||
} |
||||
|
||||
public function send(): void |
||||
{ |
||||
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 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) || 2 !== \count($parts)) { |
||||
throw new AccessDeniedException(); |
||||
} |
||||
|
||||
list($username, $password) = $parts; |
||||
|
||||
$auth = Database::getManager() |
||||
->getRepository(XApiLrsAuth::class) |
||||
->findOneBy( |
||||
['username' => $username, 'password' => $password, 'enabled' => true] |
||||
) |
||||
; |
||||
|
||||
if (null == $auth) { |
||||
throw new AccessDeniedException(); |
||||
} |
||||
|
||||
return true; |
||||
} |
||||
|
||||
private function validateVersion(): void |
||||
{ |
||||
$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 AccessDeniedException |
||||
*/ |
||||
private function generateResponse(string $controllerName, string $methodName): HttpResponse |
||||
{ |
||||
if (!class_exists($controllerName) |
||||
|| !method_exists($controllerName, $methodName) |
||||
) { |
||||
throw new NotFoundHttpException(); |
||||
} |
||||
|
||||
if (AboutController::class !== $controllerName) { |
||||
$this->validateAuth(); |
||||
$this->validateVersion(); |
||||
} |
||||
|
||||
/** @var HttpResponse $response */ |
||||
return \call_user_func( |
||||
[ |
||||
new $controllerName($this->request), |
||||
$methodName, |
||||
] |
||||
); |
||||
} |
||||
|
||||
private function alternateRequestSyntax(): void |
||||
{ |
||||
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); |
||||
} |
||||
} |
||||
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue