upgrade
This commit is contained in:
28
plugin/ims_lti/src/ContentItem/LtiContentItemType.php
Normal file
28
plugin/ims_lti/src/ContentItem/LtiContentItemType.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
/* For licensing terms, see /license.txt */
|
||||
|
||||
use Chamilo\CoreBundle\Entity\Course;
|
||||
use Chamilo\PluginBundle\Entity\ImsLti\ImsLtiTool;
|
||||
|
||||
/**
|
||||
* Class LtiContentItemType.
|
||||
*/
|
||||
abstract class LtiContentItemType
|
||||
{
|
||||
/**
|
||||
* LtiContentItemType constructor.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function __construct(stdClass $itemData)
|
||||
{
|
||||
$this->validateItemData($itemData);
|
||||
}
|
||||
|
||||
abstract public function save(ImsLtiTool $baseTool, Course $course);
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
abstract protected function validateItemData(stdClass $itemData);
|
||||
}
|
||||
150
plugin/ims_lti/src/ContentItem/LtiResourceLink.php
Normal file
150
plugin/ims_lti/src/ContentItem/LtiResourceLink.php
Normal file
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
/* For licensing terms, see /license.txt */
|
||||
|
||||
use Chamilo\CoreBundle\Entity\Course;
|
||||
use Chamilo\PluginBundle\Entity\ImsLti\ImsLtiTool;
|
||||
|
||||
/**
|
||||
* Class LtiContentItem.
|
||||
*/
|
||||
class LtiResourceLink extends LtiContentItemType
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $url;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $title;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $text;
|
||||
/**
|
||||
* @var stdClass
|
||||
*/
|
||||
private $icon;
|
||||
/**
|
||||
* @var stdClass
|
||||
*/
|
||||
private $thumbnail;
|
||||
/**
|
||||
* @var stdClass
|
||||
*/
|
||||
private $iframe;
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $custom;
|
||||
/**
|
||||
* @var stdClass
|
||||
*/
|
||||
private $lineItem;
|
||||
/**
|
||||
* @var stdClass
|
||||
*/
|
||||
private $available;
|
||||
/**
|
||||
* @var stdClass
|
||||
*/
|
||||
private $submission;
|
||||
|
||||
/**
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
*
|
||||
* @return ImsLtiTool
|
||||
*/
|
||||
public function save(ImsLtiTool $baseTool, Course $course)
|
||||
{
|
||||
$newTool = $this->createTool($baseTool);
|
||||
$newTool->setActiveDeepLinking(false);
|
||||
|
||||
$em = Database::getManager();
|
||||
|
||||
$em->persist($newTool);
|
||||
$em->flush();
|
||||
|
||||
ImsLtiPlugin::create()->addCourseTool($course, $newTool);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function validateItemData(stdClass $itemData)
|
||||
{
|
||||
$this->url = empty($itemData->url) ? '' : $itemData->url;
|
||||
$this->title = empty($itemData->title) ? '' : $itemData->title;
|
||||
$this->text = empty($itemData->text) ? '' : $itemData->text;
|
||||
$this->custom = empty($itemData->custom) || !is_array($itemData->custom) ? [] : (array) $itemData->custom;
|
||||
|
||||
$this->icon = empty($itemData->icon) ? null : $itemData->icon;
|
||||
|
||||
if ($this->icon
|
||||
&& (empty($this->icon->url) || empty($this->icon->width) || empty($this->icon->height))
|
||||
) {
|
||||
throw new Exception(sprintf("Icon properties are missing in data form content item: %s", print_r($itemData, true)));
|
||||
}
|
||||
|
||||
$this->thumbnail = empty($itemData->thumbnail) ? null : $itemData->thumbnail;
|
||||
|
||||
if ($this->thumbnail
|
||||
&& (empty($this->thumbnail->url) || empty($this->thumbnail->width) || empty($this->thumbnail->height))
|
||||
) {
|
||||
throw new Exception(sprintf("Thumbnail URL is missing in data form content item: %s", print_r($itemData, true)));
|
||||
}
|
||||
|
||||
$this->iframe = empty($itemData->iframe) ? null : $itemData->iframe;
|
||||
|
||||
if ($this->iframe && (empty($this->iframe->width) || empty($this->iframe->height))) {
|
||||
throw new Exception(sprintf("Iframe size is wrong in data form content item: %s", print_r($itemData, true)));
|
||||
}
|
||||
|
||||
$this->lineItem = empty($itemData->lineItem) ? null : $itemData->lineItem;
|
||||
|
||||
if ($this->lineItem && empty($this->lineItem->scoreMaximum)) {
|
||||
throw new Exception(sprintf("LineItem properties are missing in data form content item: %s", print_r($itemData, true)));
|
||||
}
|
||||
|
||||
$this->available = empty($itemData->available) ? null : $itemData->available;
|
||||
|
||||
if ($this->available && empty($this->available->startDateTime) && empty($this->available->endDateTime)) {
|
||||
throw new Exception(sprintf("LineItem properties are missing in data form content item: %s", print_r($itemData, true)));
|
||||
}
|
||||
|
||||
$this->submission = empty($itemData->submission) ? null : $itemData->submission;
|
||||
|
||||
if ($this->submission && empty($this->submission->startDateTime) && empty($this->submission->endDateTime)) {
|
||||
throw new Exception(sprintf("Submission properties are missing in data form content item: %s", print_r($itemData, true)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ImsLtiTool
|
||||
*/
|
||||
private function createTool(ImsLtiTool $baseTool)
|
||||
{
|
||||
$newTool = clone $baseTool;
|
||||
$newTool->setParent($baseTool);
|
||||
|
||||
if (!empty($this->url)) {
|
||||
$newTool->setLaunchUrl($this->url);
|
||||
}
|
||||
|
||||
if (!empty($this->title)) {
|
||||
$newTool->setName($this->title);
|
||||
}
|
||||
|
||||
if (!empty($this->text)) {
|
||||
$newTool->setDescription($this->text);
|
||||
}
|
||||
|
||||
if (!empty($this->custom)) {
|
||||
$newTool->setCustomParams(
|
||||
$newTool->encodeCustomParams($this->custom)
|
||||
);
|
||||
}
|
||||
|
||||
return $newTool;
|
||||
}
|
||||
}
|
||||
239
plugin/ims_lti/src/Form/FrmAdd.php
Normal file
239
plugin/ims_lti/src/Form/FrmAdd.php
Normal file
@@ -0,0 +1,239 @@
|
||||
<?php
|
||||
/* For licensing terms, see /license.txt */
|
||||
|
||||
namespace Chamilo\PluginBundle\ImsLti\Form;
|
||||
|
||||
use Category;
|
||||
use Chamilo\PluginBundle\Entity\ImsLti\ImsLtiTool;
|
||||
use Display;
|
||||
use FormValidator;
|
||||
use ImsLti;
|
||||
use ImsLtiPlugin;
|
||||
use LtiAssignmentGradesService;
|
||||
use LtiNamesRoleProvisioningService;
|
||||
|
||||
/**
|
||||
* Class FrmAdd.
|
||||
*/
|
||||
class FrmAdd extends FormValidator
|
||||
{
|
||||
/**
|
||||
* @var ImsLtiTool|null
|
||||
*/
|
||||
private $baseTool;
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $toolIsV1p3;
|
||||
|
||||
/**
|
||||
* FrmAdd constructor.
|
||||
*
|
||||
* @param string $name
|
||||
* @param array $attributes
|
||||
*/
|
||||
public function __construct(
|
||||
$name,
|
||||
$attributes = [],
|
||||
ImsLtiTool $tool = null
|
||||
) {
|
||||
parent::__construct($name, 'POST', '', '', $attributes, self::LAYOUT_HORIZONTAL, true);
|
||||
|
||||
$this->baseTool = $tool;
|
||||
$this->toolIsV1p3 = $this->baseTool
|
||||
&& !empty($this->baseTool->publicKey)
|
||||
&& !empty($this->baseTool->getClientId())
|
||||
&& !empty($this->baseTool->getLoginUrl())
|
||||
&& !empty($this->baseTool->getRedirectUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the form.
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
$plugin = ImsLtiPlugin::create();
|
||||
|
||||
$this->addHeader($plugin->get_lang('ToolSettings'));
|
||||
$this->addText('name', get_lang('Name'));
|
||||
$this->addTextarea('description', get_lang('Description'));
|
||||
|
||||
if (null === $this->baseTool) {
|
||||
$this->addUrl('launch_url', $plugin->get_lang('LaunchUrl'), true);
|
||||
$this->addRadio(
|
||||
'version',
|
||||
$plugin->get_lang('LtiVersion'),
|
||||
[
|
||||
ImsLti::V_1P1 => 'LTI 1.0 / 1.1',
|
||||
ImsLti::V_1P3 => 'LTI 1.3.0',
|
||||
]
|
||||
);
|
||||
$this->addHtml('<div class="'.ImsLti::V_1P1.'" style="display: none;">');
|
||||
$this->addText('consumer_key', $plugin->get_lang('ConsumerKey'), false);
|
||||
$this->addText('shared_secret', $plugin->get_lang('SharedSecret'), false);
|
||||
$this->addHtml('</div>');
|
||||
$this->addHtml('<div class="'.ImsLti::V_1P3.'" style="display: block;">');
|
||||
$this->addRadio(
|
||||
'public_key_type',
|
||||
$plugin->get_lang('PublicKeyType'),
|
||||
[
|
||||
ImsLti::LTI_JWK_KEYSET => $plugin->get_lang('KeySetUrl'),
|
||||
ImsLti::LTI_RSA_KEY => $plugin->get_lang('RsaKey'),
|
||||
]
|
||||
);
|
||||
$this->addHtml('<div class="'.ImsLti::LTI_JWK_KEYSET.'" style="display: block;">');
|
||||
$this->addUrl('jwks_url', $plugin->get_lang('PublicKeyset'), false);
|
||||
$this->addHtml('</div>');
|
||||
$this->addHtml('<div class="'.ImsLti::LTI_RSA_KEY.'" style="display: none;">');
|
||||
$this->addTextarea(
|
||||
'public_key',
|
||||
$plugin->get_lang('PublicKey'),
|
||||
['style' => 'font-family: monospace;', 'rows' => 5]
|
||||
);
|
||||
$this->addHtml('</div>');
|
||||
$this->addUrl('login_url', $plugin->get_lang('LoginUrl'), false);
|
||||
$this->addUrl('redirect_url', $plugin->get_lang('RedirectUrl'), false);
|
||||
$this->addHtml('</div>');
|
||||
}
|
||||
|
||||
$this->addButtonAdvancedSettings('lti_adv');
|
||||
$this->addHtml('<div id="lti_adv_options" style="display:none;">');
|
||||
$this->addTextarea(
|
||||
'custom_params',
|
||||
[$plugin->get_lang('CustomParams'), $plugin->get_lang('CustomParamsHelp')]
|
||||
);
|
||||
$this->addSelect(
|
||||
'document_target',
|
||||
get_lang('LinkTarget'),
|
||||
['iframe' => 'iframe', 'window' => 'window']
|
||||
);
|
||||
|
||||
if (null === $this->baseTool
|
||||
|| ($this->baseTool && !$this->baseTool->isActiveDeepLinking())
|
||||
) {
|
||||
$this->addCheckBox(
|
||||
'deep_linking',
|
||||
[null, $plugin->get_lang('SupportDeppLinkingHelp'), null],
|
||||
$plugin->get_lang('SupportDeepLinking')
|
||||
);
|
||||
}
|
||||
|
||||
$showAGS = false;
|
||||
|
||||
if (api_get_course_int_id()) {
|
||||
$caterories = Category::load(null, null, api_get_course_id());
|
||||
|
||||
if (!empty($caterories)) {
|
||||
$showAGS = true;
|
||||
}
|
||||
} else {
|
||||
$showAGS = true;
|
||||
}
|
||||
|
||||
$this->addHtml('<div class="'.ImsLti::V_1P3.'" style="display: none;">');
|
||||
|
||||
if ($showAGS) {
|
||||
$this->addRadio(
|
||||
'1p3_ags',
|
||||
$plugin->get_lang('AssigmentAndGradesService'),
|
||||
[
|
||||
LtiAssignmentGradesService::AGS_NONE => $plugin->get_lang('DontUseService'),
|
||||
LtiAssignmentGradesService::AGS_SIMPLE => $plugin->get_lang('AGServiceSimple'),
|
||||
LtiAssignmentGradesService::AGS_FULL => $plugin->get_lang('AGServiceFull'),
|
||||
]
|
||||
);
|
||||
} else {
|
||||
$gradebookUrl = api_get_path(WEB_CODE_PATH).'gradebook/index.php?'.api_get_cidreq();
|
||||
|
||||
$this->addLabel(
|
||||
$plugin->get_lang('AssigmentAndGradesService'),
|
||||
sprintf(
|
||||
$plugin->get_lang('YouNeedCreateTheGradebokInCourseFirst'),
|
||||
Display::url($gradebookUrl, $gradebookUrl)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$this->addRadio(
|
||||
'1p3_nrps',
|
||||
$plugin->get_lang('NamesAndRoleProvisioningService'),
|
||||
[
|
||||
LtiNamesRoleProvisioningService::NRPS_NONE => $plugin->get_lang('DontUseService'),
|
||||
LtiNamesRoleProvisioningService::NRPS_CONTEXT_MEMBERSHIP => $plugin->get_lang('UseService'),
|
||||
]
|
||||
);
|
||||
$this->addHtml('</div>');
|
||||
|
||||
if (!$this->baseTool) {
|
||||
$this->addText(
|
||||
'replacement_user_id',
|
||||
[
|
||||
$plugin->get_lang('ReplacementUserId'),
|
||||
$plugin->get_lang('ReplacementUserIdHelp'),
|
||||
],
|
||||
false
|
||||
);
|
||||
$this->applyFilter('replacement_user_id', 'trim');
|
||||
}
|
||||
|
||||
$this->addHtml('</div>');
|
||||
$this->addButtonAdvancedSettings('lti_privacy', get_lang('Privacy'));
|
||||
$this->addHtml('<div id="lti_privacy_options" style="display:none;">');
|
||||
$this->addCheckBox('share_name', null, $plugin->get_lang('ShareLauncherName'));
|
||||
$this->addCheckBox('share_email', null, $plugin->get_lang('ShareLauncherEmail'));
|
||||
$this->addCheckBox('share_picture', null, $plugin->get_lang('ShareLauncherPicture'));
|
||||
$this->addHtml('</div>');
|
||||
$this->addButtonCreate($plugin->get_lang('AddExternalTool'));
|
||||
$this->applyFilter('__ALL__', 'trim');
|
||||
}
|
||||
|
||||
public function setDefaultValues()
|
||||
{
|
||||
$defaults = [];
|
||||
$defaults['version'] = ImsLti::V_1P3;
|
||||
$defaults['public_key_type'] = ImsLti::LTI_JWK_KEYSET;
|
||||
|
||||
if ($this->baseTool) {
|
||||
$defaults['name'] = $this->baseTool->getName();
|
||||
$defaults['description'] = $this->baseTool->getDescription();
|
||||
$defaults['custom_params'] = $this->baseTool->getCustomParams();
|
||||
$defaults['document_target'] = $this->baseTool->getDocumentTarget();
|
||||
$defaults['share_name'] = $this->baseTool->isSharingName();
|
||||
$defaults['share_email'] = $this->baseTool->isSharingEmail();
|
||||
$defaults['share_picture'] = $this->baseTool->isSharingPicture();
|
||||
$defaults['public_key'] = $this->baseTool->publicKey;
|
||||
$defaults['login_url'] = $this->baseTool->getLoginUrl();
|
||||
$defaults['redirect_url'] = $this->baseTool->getRedirectUrl();
|
||||
|
||||
if ($this->toolIsV1p3) {
|
||||
$advServices = $this->baseTool->getAdvantageServices();
|
||||
|
||||
$defaults['1p3_ags'] = $advServices['ags'];
|
||||
$defaults['1p3_nrps'] = $advServices['nrps'];
|
||||
}
|
||||
}
|
||||
|
||||
$this->setDefaults($defaults);
|
||||
}
|
||||
|
||||
public function returnForm(): string
|
||||
{
|
||||
$js = "<script>
|
||||
\$(function () {
|
||||
\$('[name=\"version\"]').on('change', function () {
|
||||
$('.".ImsLti::V_1P1.", .".ImsLti::V_1P3."').hide();
|
||||
|
||||
$('.' + this.value).show();
|
||||
})
|
||||
\$('[name=\"public_key_type\"]').on('change', function () {
|
||||
$('.".ImsLti::LTI_JWK_KEYSET.", .".ImsLti::LTI_RSA_KEY."').hide();
|
||||
$('[name=\"public_key\"], [name=\"jwks_url\"]').val('');
|
||||
|
||||
$('.' + this.value).show();
|
||||
})
|
||||
});
|
||||
</script>";
|
||||
|
||||
return $js.parent::returnForm(); // TODO: Change the autogenerated stub
|
||||
}
|
||||
}
|
||||
224
plugin/ims_lti/src/Form/FrmEdit.php
Normal file
224
plugin/ims_lti/src/Form/FrmEdit.php
Normal file
@@ -0,0 +1,224 @@
|
||||
<?php
|
||||
/* For licensing terms, see /license.txt */
|
||||
|
||||
namespace Chamilo\PluginBundle\Form;
|
||||
|
||||
use Category;
|
||||
use Chamilo\PluginBundle\Entity\ImsLti\ImsLtiTool;
|
||||
use Display;
|
||||
use Exception;
|
||||
use FormValidator;
|
||||
use ImsLti;
|
||||
use ImsLtiPlugin;
|
||||
use LtiAssignmentGradesService;
|
||||
use LtiNamesRoleProvisioningService;
|
||||
|
||||
/**
|
||||
* Class FrmAdd.
|
||||
*/
|
||||
class FrmEdit extends FormValidator
|
||||
{
|
||||
/**
|
||||
* @var ImsLtiTool|null
|
||||
*/
|
||||
private $tool;
|
||||
|
||||
/**
|
||||
* FrmAdd constructor.
|
||||
*
|
||||
* @param string $name
|
||||
* @param array $attributes
|
||||
*/
|
||||
public function __construct(
|
||||
$name,
|
||||
$attributes = [],
|
||||
ImsLtiTool $tool = null
|
||||
) {
|
||||
parent::__construct($name, 'POST', '', '', $attributes, self::LAYOUT_HORIZONTAL, true);
|
||||
|
||||
$this->tool = $tool;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the form.
|
||||
*
|
||||
* @param bool $globalMode
|
||||
*/
|
||||
public function build($globalMode = true)
|
||||
{
|
||||
$plugin = ImsLtiPlugin::create();
|
||||
$course = $this->tool->getCourse();
|
||||
$parent = $this->tool->getParent();
|
||||
|
||||
$this->addHeader($plugin->get_lang('ToolSettings'));
|
||||
|
||||
if (null !== $course && $globalMode) {
|
||||
$this->addHtml(
|
||||
Display::return_message(
|
||||
sprintf($plugin->get_lang('ToolAddedOnCourseX'), $course->getTitle()),
|
||||
'normal',
|
||||
false
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$this->addText('name', get_lang('Name'));
|
||||
$this->addTextarea('description', get_lang('Description'));
|
||||
$this->addRadio(
|
||||
'version',
|
||||
$plugin->get_lang('LtiVersion'),
|
||||
[
|
||||
ImsLti::V_1P1 => 'LTI 1.0 / 1.1',
|
||||
ImsLti::V_1P3 => 'LTI 1.3.0',
|
||||
]
|
||||
);
|
||||
$this->freeze(['version']);
|
||||
|
||||
if (null === $parent) {
|
||||
$this->addUrl('launch_url', $plugin->get_lang('LaunchUrl'), true);
|
||||
if ($this->tool->getVersion() === ImsLti::V_1P1) {
|
||||
$this->addText('consumer_key', $plugin->get_lang('ConsumerKey'), false);
|
||||
$this->addText('shared_secret', $plugin->get_lang('SharedSecret'), false);
|
||||
} elseif ($this->tool->getVersion() === ImsLti::V_1P3) {
|
||||
$this->addText('client_id', $plugin->get_lang('ClientId'), true);
|
||||
$this->freeze(['client_id']);
|
||||
if (!empty($this->tool->getJwksUrl())) {
|
||||
$this->addUrl('jwks_url', $plugin->get_lang('PublicKeyset'));
|
||||
} else {
|
||||
$this->addTextarea(
|
||||
'public_key',
|
||||
$plugin->get_lang('PublicKey'),
|
||||
['style' => 'font-family: monospace;', 'rows' => 5],
|
||||
true
|
||||
);
|
||||
}
|
||||
$this->addUrl('login_url', $plugin->get_lang('LoginUrl'));
|
||||
$this->addUrl('redirect_url', $plugin->get_lang('RedirectUrl'));
|
||||
}
|
||||
}
|
||||
|
||||
$this->addButtonAdvancedSettings('lti_adv');
|
||||
$this->addHtml('<div id="lti_adv_options" style="display:none;">');
|
||||
$this->addTextarea(
|
||||
'custom_params',
|
||||
[$plugin->get_lang('CustomParams'), $plugin->get_lang('CustomParamsHelp')]
|
||||
);
|
||||
$this->addSelect(
|
||||
'document_target',
|
||||
get_lang('LinkTarget'),
|
||||
['iframe' => 'iframe', 'window' => 'window']
|
||||
);
|
||||
|
||||
if (null === $parent
|
||||
|| (null !== $parent && !$parent->isActiveDeepLinking())
|
||||
) {
|
||||
$this->addCheckBox(
|
||||
'deep_linking',
|
||||
[null, $plugin->get_lang('SupportDeppLinkingHelp'), null],
|
||||
$plugin->get_lang('SupportDeepLinking')
|
||||
);
|
||||
}
|
||||
|
||||
if (null === $parent && $this->tool->getVersion() === ImsLti::V_1P3) {
|
||||
$showAGS = false;
|
||||
|
||||
if (api_get_course_int_id()) {
|
||||
$caterories = Category::load(null, null, api_get_course_id());
|
||||
|
||||
if (!empty($caterories)) {
|
||||
$showAGS = true;
|
||||
}
|
||||
} else {
|
||||
$showAGS = true;
|
||||
}
|
||||
|
||||
if ($showAGS) {
|
||||
$this->addRadio(
|
||||
'1p3_ags',
|
||||
$plugin->get_lang('AssigmentAndGradesService'),
|
||||
[
|
||||
LtiAssignmentGradesService::AGS_NONE => $plugin->get_lang('DontUseService'),
|
||||
LtiAssignmentGradesService::AGS_SIMPLE => $plugin->get_lang('AGServiceSimple'),
|
||||
LtiAssignmentGradesService::AGS_FULL => $plugin->get_lang('AGServiceFull'),
|
||||
]
|
||||
);
|
||||
} else {
|
||||
$gradebookUrl = api_get_path(WEB_CODE_PATH).'gradebook/index.php?'.api_get_cidreq();
|
||||
|
||||
$this->addLabel(
|
||||
$plugin->get_lang('AssigmentAndGradesService'),
|
||||
sprintf(
|
||||
$plugin->get_lang('YouNeedCreateTheGradebokInCourseFirst'),
|
||||
Display::url($gradebookUrl, $gradebookUrl)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$this->addRadio(
|
||||
'1p3_nrps',
|
||||
$plugin->get_lang('NamesAndRoleProvisioningService'),
|
||||
[
|
||||
LtiNamesRoleProvisioningService::NRPS_NONE => $plugin->get_lang('DontUseService'),
|
||||
LtiNamesRoleProvisioningService::NRPS_CONTEXT_MEMBERSHIP => $plugin->get_lang('UseService'),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
if (!$parent) {
|
||||
$this->addText(
|
||||
'replacement_user_id',
|
||||
[
|
||||
$plugin->get_lang('ReplacementUserId'),
|
||||
$plugin->get_lang('ReplacementUserIdHelp'),
|
||||
],
|
||||
false
|
||||
);
|
||||
$this->applyFilter('replacement_user_id', 'trim');
|
||||
}
|
||||
|
||||
$this->addHtml('</div>');
|
||||
$this->addButtonAdvancedSettings('lti_privacy', get_lang('Privacy'));
|
||||
$this->addHtml('<div id="lti_privacy_options" style="display:none;">');
|
||||
$this->addCheckBox('share_name', null, $plugin->get_lang('ShareLauncherName'));
|
||||
$this->addCheckBox('share_email', null, $plugin->get_lang('ShareLauncherEmail'));
|
||||
$this->addCheckBox('share_picture', null, $plugin->get_lang('ShareLauncherPicture'));
|
||||
$this->addHtml('</div>');
|
||||
$this->addButtonUpdate($plugin->get_lang('EditExternalTool'));
|
||||
$this->addHidden('id', $this->tool->getId());
|
||||
$this->addHidden('action', 'edit');
|
||||
$this->applyFilter('__ALL__', 'trim');
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function setDefaultValues()
|
||||
{
|
||||
$advServices = $this->tool->getAdvantageServices();
|
||||
|
||||
$this->setDefaults(
|
||||
[
|
||||
'name' => $this->tool->getName(),
|
||||
'description' => $this->tool->getDescription(),
|
||||
'launch_url' => $this->tool->getLaunchUrl(),
|
||||
'consumer_key' => $this->tool->getConsumerKey(),
|
||||
'shared_secret' => $this->tool->getSharedSecret(),
|
||||
'custom_params' => $this->tool->getCustomParams(),
|
||||
'deep_linking' => $this->tool->isActiveDeepLinking(),
|
||||
'share_name' => $this->tool->isSharingName(),
|
||||
'share_email' => $this->tool->isSharingEmail(),
|
||||
'share_picture' => $this->tool->isSharingPicture(),
|
||||
'version' => $this->tool->getVersion(),
|
||||
'client_id' => $this->tool->getClientId(),
|
||||
'public_key' => $this->tool->publicKey,
|
||||
'jwks_url' => $this->tool->getJwksUrl(),
|
||||
'login_url' => $this->tool->getLoginUrl(),
|
||||
'redirect_url' => $this->tool->getRedirectUrl(),
|
||||
'1p3_ags' => $advServices['ags'],
|
||||
'1p3_nrps' => $advServices['nrps'],
|
||||
'document_target' => $this->tool->getDocumentTarget(),
|
||||
'replacement_user_id' => $this->tool->getReplacementForUserId(),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
240
plugin/ims_lti/src/ImsLti.php
Normal file
240
plugin/ims_lti/src/ImsLti.php
Normal file
@@ -0,0 +1,240 @@
|
||||
<?php
|
||||
/* For licensing terms, see /license.txt */
|
||||
|
||||
use Chamilo\CoreBundle\Entity\Course;
|
||||
use Chamilo\CoreBundle\Entity\Session;
|
||||
use Chamilo\PluginBundle\Entity\ImsLti\ImsLtiTool;
|
||||
use Chamilo\UserBundle\Entity\User;
|
||||
|
||||
/**
|
||||
* Class ImsLti.
|
||||
*/
|
||||
class ImsLti
|
||||
{
|
||||
const V_1P1 = 'lti1p1';
|
||||
const V_1P3 = 'lti1p3';
|
||||
const LTI_RSA_KEY = 'rsa_key';
|
||||
const LTI_JWK_KEYSET = 'jwk_keyset';
|
||||
|
||||
/**
|
||||
* @param Session|null $session Optional.
|
||||
* @param string $domain Optional. Institution domain.
|
||||
* @param string $ltiVersion Optional. Default is lti1p1.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getSubstitutableVariables(
|
||||
User $user,
|
||||
Course $course,
|
||||
Session $session = null,
|
||||
$domain = '',
|
||||
$ltiVersion = self::V_1P1,
|
||||
ImsLtiTool $tool
|
||||
) {
|
||||
$isLti1p3 = $ltiVersion === self::V_1P3;
|
||||
|
||||
return [
|
||||
'$User.id' => $user->getId(),
|
||||
'$User.image' => $isLti1p3 ? ['claim' => 'sub'] : ['user_image'],
|
||||
'$User.username' => $user->getUsername(),
|
||||
'$User.org' => false,
|
||||
'$User.scope.mentor' => $isLti1p3 ? ['claim' => '/claim/role_scope_mentor'] : ['role_scope_mentor'],
|
||||
|
||||
'$Person.sourcedId' => $isLti1p3
|
||||
? self::getPersonSourcedId($domain, $user)
|
||||
: "$domain:".ImsLtiPlugin::getLaunchUserIdClaim($tool, $user),
|
||||
'$Person.name.full' => $user->getFullname(),
|
||||
'$Person.name.family' => $user->getLastname(),
|
||||
'$Person.name.given' => $user->getFirstname(),
|
||||
'$Person.address.street1' => $user->getAddress(),
|
||||
'$Person.phone.primary' => $user->getPhone(),
|
||||
'$Person.email.primary' => $user->getEmail(),
|
||||
|
||||
'$CourseSection.sourcedId' => $isLti1p3
|
||||
? ['claim' => '/claim/lis', 'property' => 'course_section_sourcedid']
|
||||
: ['lis_course_section_sourcedid'],
|
||||
'$CourseSection.label' => $course->getCode(),
|
||||
'$CourseSection.title' => $course->getTitle(),
|
||||
'$CourseSection.longDescription' => $session && $session->getShowDescription()
|
||||
? $session->getDescription()
|
||||
: false,
|
||||
'$CourseSection.timeFrame.begin' => $session && $session->getDisplayStartDate()
|
||||
? $session->getDisplayStartDate()->format(DateTime::ATOM)
|
||||
: '$CourseSection.timeFrame.begin',
|
||||
'$CourseSection.timeFrame.end' => $session && $session->getDisplayEndDate()
|
||||
? $session->getDisplayEndDate()->format(DateTime::ATOM)
|
||||
: '$CourseSection.timeFrame.end',
|
||||
|
||||
'$Membership.role' => $isLti1p3 ? ['claim' => '/claim/roles'] : ['roles'],
|
||||
|
||||
'$Result.sourcedGUID' => $isLti1p3 ? ['claim' => 'sub'] : ['lis_result_sourcedid'],
|
||||
'$Result.sourcedId' => $isLti1p3 ? ['claim' => 'sub'] : ['lis_result_sourcedid'],
|
||||
|
||||
'$ResourceLink.id' => $isLti1p3
|
||||
? ['claim' => '/claim/resource_link', 'property' => 'id']
|
||||
: ['resource_link_id'],
|
||||
'$ResourceLink.title' => $isLti1p3
|
||||
? ['claim' => '/claim/resource_link', 'property' => 'title']
|
||||
: ['resource_link_title'],
|
||||
'$ResourceLink.description' => $isLti1p3
|
||||
? ['claim' => '/claim/resource_link', 'property' => 'description']
|
||||
: ['resource_link_description'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $launchParams All params for launch.
|
||||
* @param array $customParams Custom params where search variables to substitute.
|
||||
* @param Session|null $session Optional.
|
||||
* @param string $domain Optional. Institution domain.
|
||||
* @param string $ltiVersion Optional. Default is lti1p1.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function substituteVariablesInCustomParams(
|
||||
array $launchParams,
|
||||
array $customParams,
|
||||
User $user,
|
||||
Course $course,
|
||||
Session $session = null,
|
||||
$domain = '',
|
||||
$ltiVersion = self::V_1P1,
|
||||
ImsLtiTool $tool
|
||||
) {
|
||||
$substitutables = self::getSubstitutableVariables($user, $course, $session, $domain, $ltiVersion, $tool);
|
||||
$variables = array_keys($substitutables);
|
||||
|
||||
foreach ($customParams as $customKey => $customValue) {
|
||||
if (!in_array($customValue, $variables)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$substitute = $substitutables[$customValue];
|
||||
|
||||
if (is_array($substitute)) {
|
||||
if ($ltiVersion === self::V_1P1) {
|
||||
$substitute = current($substitute);
|
||||
|
||||
$substitute = $launchParams[$substitute];
|
||||
} elseif ($ltiVersion === self::V_1P3) {
|
||||
$claim = array_key_exists($substitute['claim'], $launchParams)
|
||||
? $substitute['claim']
|
||||
: "https://purl.imsglobal.org/spec/lti{$substitute['claim']}";
|
||||
|
||||
$substitute = empty($substitute['property'])
|
||||
? $launchParams[$claim]
|
||||
: $launchParams[$claim][$substitute['property']];
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$customParams[$customKey] = $substitute;
|
||||
}
|
||||
|
||||
array_walk_recursive(
|
||||
$customParams,
|
||||
function (&$value) {
|
||||
if (gettype($value) !== 'array') {
|
||||
$value = (string) $value;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return $customParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a user sourced ID for LIS.
|
||||
*
|
||||
* @param string $domain
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getPersonSourcedId($domain, User $user)
|
||||
{
|
||||
$sourceId = [$domain, $user->getId()];
|
||||
|
||||
return implode(':', $sourceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a course sourced ID for LIS.
|
||||
*
|
||||
* @param string $domain
|
||||
* @param Session $session Optional.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getCourseSectionSourcedId($domain, Course $course, Session $session = null)
|
||||
{
|
||||
$sourceId = [$domain, $course->getId()];
|
||||
|
||||
if ($session) {
|
||||
$sourceId[] = $session->getId();
|
||||
}
|
||||
|
||||
return implode(':', $sourceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get instances for LTI Advantage services.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getAdvantageServices(ImsLtiTool $tool)
|
||||
{
|
||||
return [
|
||||
new LtiAssignmentGradesService($tool),
|
||||
new LtiNamesRoleProvisioningService($tool),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $length
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function generateClientId($length = 20)
|
||||
{
|
||||
$hash = md5(mt_rand().time());
|
||||
|
||||
$clientId = '';
|
||||
|
||||
for ($p = 0; $p < $length; $p++) {
|
||||
$op = mt_rand(1, 3);
|
||||
|
||||
if ($op === 1) {
|
||||
$char = chr(mt_rand(97, 97 + 25));
|
||||
} elseif ($op === 2) {
|
||||
$char = chr(mt_rand(65, 65 + 25));
|
||||
} else {
|
||||
$char = substr($hash, mt_rand(0, strlen($hash) - 1), 1);
|
||||
}
|
||||
|
||||
$clientId .= $char;
|
||||
}
|
||||
|
||||
return $clientId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the format ISO 8601 for date strings coming from JSON or JavaScript.
|
||||
*
|
||||
* @see https://www.myintervals.com/blog/2009/05/20/iso-8601-date-validation-that-doesnt-suck/ Pattern source.
|
||||
*
|
||||
* @param string $strDate
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function validateFormatDateIso8601($strDate)
|
||||
{
|
||||
$pattern = '/^([\+-]?\d{4}(?!\d{2}\b))((-?)('
|
||||
.'(0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W'
|
||||
.'([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))('
|
||||
.'[T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24\:?00)([\.,]\d+(?!:))?)?(\17[0-5]\d([\.,]\d+)?)?'
|
||||
.'([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/';
|
||||
|
||||
return preg_match($pattern, $strDate) !== false;
|
||||
}
|
||||
}
|
||||
72
plugin/ims_lti/src/ImsLtiServiceDeleteRequest.php
Normal file
72
plugin/ims_lti/src/ImsLtiServiceDeleteRequest.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
/* For licensing terms, see /license.txt */
|
||||
|
||||
use Chamilo\CoreBundle\Entity\GradebookEvaluation;
|
||||
use Chamilo\UserBundle\Entity\User;
|
||||
|
||||
/**
|
||||
* Class ImsLtiDeleteServiceRequest.
|
||||
*/
|
||||
class ImsLtiServiceDeleteRequest extends ImsLtiServiceRequest
|
||||
{
|
||||
/**
|
||||
* ImsLtiDeleteServiceRequest constructor.
|
||||
*/
|
||||
public function __construct(SimpleXMLElement $xml)
|
||||
{
|
||||
parent::__construct($xml);
|
||||
|
||||
$this->responseType = ImsLtiServiceResponse::TYPE_DELETE;
|
||||
$this->xmlRequest = $this->xmlRequest->deleteResultRequest;
|
||||
}
|
||||
|
||||
protected function processBody()
|
||||
{
|
||||
$resultRecord = $this->xmlRequest->resultRecord;
|
||||
$sourcedId = (string) $resultRecord->sourcedGUID->sourcedId;
|
||||
$sourcedId = htmlspecialchars_decode($sourcedId);
|
||||
|
||||
$sourcedParts = json_decode($sourcedId, true);
|
||||
|
||||
if (empty($sourcedParts)) {
|
||||
$this->statusInfo
|
||||
->setSeverity(ImsLtiServiceResponseStatus::SEVERITY_ERROR)
|
||||
->setCodeMajor(ImsLtiServiceResponseStatus::CODEMAJOR_FAILURE);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$em = Database::getManager();
|
||||
/** @var GradebookEvaluation $evaluation */
|
||||
$evaluation = $em->find('ChamiloCoreBundle:GradebookEvaluation', $sourcedParts['e']);
|
||||
/** @var User $user */
|
||||
$user = $em->find('ChamiloUserBundle:User', $sourcedParts['u']);
|
||||
|
||||
if (empty($evaluation) || empty($user)) {
|
||||
$this->statusInfo
|
||||
->setSeverity(ImsLtiServiceResponseStatus::SEVERITY_STATUS)
|
||||
->setCodeMajor(ImsLtiServiceResponseStatus::CODEMAJOR_FAILURE);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$results = Result::load(null, $user->getId(), $evaluation->getId());
|
||||
|
||||
if (empty($results)) {
|
||||
$this->statusInfo
|
||||
->setSeverity(ImsLtiServiceResponseStatus::SEVERITY_STATUS)
|
||||
->setCodeMajor(ImsLtiServiceResponseStatus::CODEMAJOR_FAILURE);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var Result $result */
|
||||
$result = $results[0];
|
||||
$result->addResultLog($user->getId(), $evaluation->getId());
|
||||
$result->delete();
|
||||
|
||||
$this->statusInfo
|
||||
->setSeverity(ImsLtiServiceResponseStatus::SEVERITY_STATUS)
|
||||
->setCodeMajor(ImsLtiServiceResponseStatus::CODEMAJOR_SUCCESS);
|
||||
}
|
||||
}
|
||||
25
plugin/ims_lti/src/ImsLtiServiceDeleteResponse.php
Normal file
25
plugin/ims_lti/src/ImsLtiServiceDeleteResponse.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
/* For licensing terms, see /license.txt */
|
||||
|
||||
/**
|
||||
* Class ImsLtiServiceDeleteResponse.
|
||||
*/
|
||||
class ImsLtiServiceDeleteResponse extends ImsLtiServiceResponse
|
||||
{
|
||||
/**
|
||||
* ImsLtiServiceDeleteResponse constructor.
|
||||
*
|
||||
* @param mixed|null $bodyParam
|
||||
*/
|
||||
public function __construct(ImsLtiServiceResponseStatus $statusInfo, $bodyParam = null)
|
||||
{
|
||||
$statusInfo->setOperationRefIdentifier('deleteResult');
|
||||
|
||||
parent::__construct($statusInfo, $bodyParam);
|
||||
}
|
||||
|
||||
protected function generateBody(SimpleXMLElement $xmlBody)
|
||||
{
|
||||
$xmlBody->addChild('deleteResultResponse');
|
||||
}
|
||||
}
|
||||
81
plugin/ims_lti/src/ImsLtiServiceReadRequest.php
Normal file
81
plugin/ims_lti/src/ImsLtiServiceReadRequest.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
/* For licensing terms, see /license.txt */
|
||||
|
||||
use Chamilo\CoreBundle\Entity\GradebookEvaluation;
|
||||
use Chamilo\UserBundle\Entity\User;
|
||||
|
||||
/**
|
||||
* Class ImsLtiServiceReadRequest.
|
||||
*/
|
||||
class ImsLtiServiceReadRequest extends ImsLtiServiceRequest
|
||||
{
|
||||
/**
|
||||
* ImsLtiServiceReadRequest constructor.
|
||||
*/
|
||||
public function __construct(SimpleXMLElement $xml)
|
||||
{
|
||||
parent::__construct($xml);
|
||||
|
||||
$this->responseType = ImsLtiServiceResponse::TYPE_READ;
|
||||
$this->xmlRequest = $this->xmlRequest->readResultRequest;
|
||||
}
|
||||
|
||||
protected function processBody()
|
||||
{
|
||||
$resultRecord = $this->xmlRequest->resultRecord;
|
||||
$sourcedId = (string) $resultRecord->sourcedGUID->sourcedId;
|
||||
$sourcedId = htmlspecialchars_decode($sourcedId);
|
||||
|
||||
$sourcedParts = json_decode($sourcedId, true);
|
||||
|
||||
if (empty($sourcedParts)) {
|
||||
$this->statusInfo
|
||||
->setSeverity(ImsLtiServiceResponseStatus::SEVERITY_ERROR)
|
||||
->setCodeMajor(ImsLtiServiceResponseStatus::CODEMAJOR_FAILURE);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$em = Database::getManager();
|
||||
/** @var GradebookEvaluation $evaluation */
|
||||
$evaluation = $em->find('ChamiloCoreBundle:GradebookEvaluation', $sourcedParts['e']);
|
||||
/** @var User $user */
|
||||
$user = $em->find('ChamiloUserBundle:User', $sourcedParts['u']);
|
||||
|
||||
if (empty($evaluation) || empty($user)) {
|
||||
$this->statusInfo
|
||||
->setSeverity(ImsLtiServiceResponseStatus::SEVERITY_STATUS)
|
||||
->setCodeMajor(ImsLtiServiceResponseStatus::CODEMAJOR_FAILURE);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$results = Result::load(null, $user->getId(), $evaluation->getId());
|
||||
|
||||
$ltiScore = '';
|
||||
$responseDescription = get_plugin_lang('ScoreNotSet', 'ImsLtiPlugin');
|
||||
|
||||
if (!empty($results)) {
|
||||
/** @var Result $result */
|
||||
$result = $results[0];
|
||||
$ltiScore = 0;
|
||||
|
||||
if (!empty($result->get_score())) {
|
||||
$ltiScore = $result->get_score() / $evaluation->getMax();
|
||||
}
|
||||
|
||||
$responseDescription = sprintf(
|
||||
get_plugin_lang('ScoreForXUserIsYScore', 'ImsLtiPlugin'),
|
||||
$user->getId(),
|
||||
$ltiScore
|
||||
);
|
||||
}
|
||||
|
||||
$this->statusInfo
|
||||
->setSeverity(ImsLtiServiceResponseStatus::SEVERITY_STATUS)
|
||||
->setCodeMajor(ImsLtiServiceResponseStatus::CODEMAJOR_SUCCESS)
|
||||
->setDescription($responseDescription);
|
||||
|
||||
$this->responseBodyParam = (string) $ltiScore;
|
||||
}
|
||||
}
|
||||
31
plugin/ims_lti/src/ImsLtiServiceReadResponse.php
Normal file
31
plugin/ims_lti/src/ImsLtiServiceReadResponse.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
/* For licensing terms, see /license.txt */
|
||||
|
||||
/**
|
||||
* Class ImsLtiReadServiceResponse.
|
||||
*/
|
||||
class ImsLtiServiceReadResponse extends ImsLtiServiceResponse
|
||||
{
|
||||
/**
|
||||
* ImsLtiServiceReadResponse constructor.
|
||||
*
|
||||
* @param mixed|null $bodyParam
|
||||
*/
|
||||
public function __construct(ImsLtiServiceResponseStatus $statusInfo, $bodyParam = null)
|
||||
{
|
||||
$statusInfo->setOperationRefIdentifier('readResult');
|
||||
|
||||
parent::__construct($statusInfo, $bodyParam);
|
||||
}
|
||||
|
||||
protected function generateBody(SimpleXMLElement $xmlBody)
|
||||
{
|
||||
$resultResponse = $xmlBody->addChild('readResultResponse');
|
||||
|
||||
$xmlResultScore = $resultResponse->addChild('result')
|
||||
->addChild('resultScore');
|
||||
|
||||
$xmlResultScore->addChild('language', 'en');
|
||||
$xmlResultScore->addChild('textString', $this->bodyParams);
|
||||
}
|
||||
}
|
||||
101
plugin/ims_lti/src/ImsLtiServiceReplaceRequest.php
Normal file
101
plugin/ims_lti/src/ImsLtiServiceReplaceRequest.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
/* For licensing terms, see /license.txt */
|
||||
|
||||
use Chamilo\CoreBundle\Entity\GradebookEvaluation;
|
||||
use Chamilo\UserBundle\Entity\User;
|
||||
|
||||
/**
|
||||
* Class ImsLtiReplaceServiceRequest.
|
||||
*/
|
||||
class ImsLtiServiceReplaceRequest extends ImsLtiServiceRequest
|
||||
{
|
||||
/**
|
||||
* ImsLtiReplaceServiceRequest constructor.
|
||||
*/
|
||||
public function __construct(SimpleXMLElement $xml)
|
||||
{
|
||||
parent::__construct($xml);
|
||||
|
||||
$this->responseType = ImsLtiServiceResponse::TYPE_REPLACE;
|
||||
$this->xmlRequest = $this->xmlRequest->replaceResultRequest;
|
||||
}
|
||||
|
||||
protected function processBody()
|
||||
{
|
||||
$resultRecord = $this->xmlRequest->resultRecord;
|
||||
$sourcedId = (string) $resultRecord->sourcedGUID->sourcedId;
|
||||
$sourcedId = htmlspecialchars_decode($sourcedId);
|
||||
$resultScore = (string) $resultRecord->result->resultScore->textString;
|
||||
|
||||
if (!is_numeric($resultScore)) {
|
||||
$this->statusInfo
|
||||
->setSeverity(ImsLtiServiceResponseStatus::SEVERITY_ERROR)
|
||||
->setCodeMajor(ImsLtiServiceResponseStatus::CODEMAJOR_FAILURE);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$resultScore = (float) $resultScore;
|
||||
|
||||
if (0 > $resultScore || 1 < $resultScore) {
|
||||
$this->statusInfo
|
||||
->setSeverity(ImsLtiServiceResponseStatus::SEVERITY_WARNING)
|
||||
->setCodeMajor(ImsLtiServiceResponseStatus::CODEMAJOR_FAILURE);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$sourcedParts = json_decode($sourcedId, true);
|
||||
|
||||
if (empty($sourcedParts)) {
|
||||
$this->statusInfo
|
||||
->setSeverity(ImsLtiServiceResponseStatus::SEVERITY_ERROR)
|
||||
->setCodeMajor(ImsLtiServiceResponseStatus::CODEMAJOR_FAILURE);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$em = Database::getManager();
|
||||
/** @var GradebookEvaluation $evaluation */
|
||||
$evaluation = $em->find('ChamiloCoreBundle:GradebookEvaluation', $sourcedParts['e']);
|
||||
/** @var User $user */
|
||||
$user = $em->find('ChamiloUserBundle:User', $sourcedParts['u']);
|
||||
|
||||
if (empty($evaluation) || empty($user)) {
|
||||
$this->statusInfo
|
||||
->setSeverity(ImsLtiServiceResponseStatus::SEVERITY_STATUS)
|
||||
->setCodeMajor(ImsLtiServiceResponseStatus::CODEMAJOR_FAILURE);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$score = $evaluation->getMax() * $resultScore;
|
||||
|
||||
$results = Result::load(null, $user->getId(), $evaluation->getId());
|
||||
|
||||
if (empty($results)) {
|
||||
$result = new Result();
|
||||
$result->set_evaluation_id($evaluation->getId());
|
||||
$result->set_user_id($user->getId());
|
||||
$result->set_score($score);
|
||||
$result->add();
|
||||
} else {
|
||||
/** @var Result $result */
|
||||
$result = $results[0];
|
||||
$result->addResultLog($user->getId(), $evaluation->getId());
|
||||
$result->set_score($score);
|
||||
$result->save();
|
||||
}
|
||||
|
||||
$this->statusInfo
|
||||
->setSeverity(ImsLtiServiceResponseStatus::SEVERITY_STATUS)
|
||||
->setCodeMajor(ImsLtiServiceResponseStatus::CODEMAJOR_SUCCESS)
|
||||
->setDescription(
|
||||
sprintf(
|
||||
get_plugin_lang('ScoreForXUserIsYScore', 'ImsLtiPlugin'),
|
||||
$user->getId(),
|
||||
$resultScore
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
25
plugin/ims_lti/src/ImsLtiServiceReplaceResponse.php
Normal file
25
plugin/ims_lti/src/ImsLtiServiceReplaceResponse.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
/* For licensing terms, see /license.txt */
|
||||
|
||||
/**
|
||||
* Class ImsLtiReplaceServiceResponse.
|
||||
*/
|
||||
class ImsLtiServiceReplaceResponse extends ImsLtiServiceResponse
|
||||
{
|
||||
/**
|
||||
* ImsLtiServiceReplaceResponse constructor.
|
||||
*
|
||||
* @param mixed|null $bodyParam
|
||||
*/
|
||||
public function __construct(ImsLtiServiceResponseStatus $statusInfo, $bodyParam = null)
|
||||
{
|
||||
$statusInfo->setOperationRefIdentifier('replaceResult');
|
||||
|
||||
parent::__construct($statusInfo, $bodyParam);
|
||||
}
|
||||
|
||||
protected function generateBody(SimpleXMLElement $xmlBody)
|
||||
{
|
||||
$xmlBody->addChild('replaceResultResponse');
|
||||
}
|
||||
}
|
||||
80
plugin/ims_lti/src/ImsLtiServiceRequest.php
Normal file
80
plugin/ims_lti/src/ImsLtiServiceRequest.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
/* For licensing terms, see /license.txt */
|
||||
|
||||
/**
|
||||
* Class ImsLtiServiceRequest.
|
||||
*/
|
||||
abstract class ImsLtiServiceRequest
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $responseType;
|
||||
|
||||
/**
|
||||
* @var SimpleXMLElement
|
||||
*/
|
||||
protected $xmlHeaderInfo;
|
||||
|
||||
/**
|
||||
* @var SimpleXMLElement
|
||||
*/
|
||||
protected $xmlRequest;
|
||||
|
||||
/**
|
||||
* @var ImsLtiServiceResponseStatus
|
||||
*/
|
||||
protected $statusInfo;
|
||||
|
||||
/**
|
||||
* @var mixed
|
||||
*/
|
||||
protected $responseBodyParam;
|
||||
|
||||
/**
|
||||
* ImsLtiServiceRequest constructor.
|
||||
*/
|
||||
public function __construct(SimpleXMLElement $xml)
|
||||
{
|
||||
$this->statusInfo = new ImsLtiServiceResponseStatus();
|
||||
|
||||
$this->xmlHeaderInfo = $xml->imsx_POXHeader->imsx_POXRequestHeaderInfo;
|
||||
$this->xmlRequest = $xml->imsx_POXBody->children();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ImsLtiServiceResponse|null
|
||||
*/
|
||||
public function process()
|
||||
{
|
||||
$this->processHeader();
|
||||
$this->processBody();
|
||||
|
||||
return $this->generateResponse();
|
||||
}
|
||||
|
||||
protected function processHeader()
|
||||
{
|
||||
$info = $this->xmlHeaderInfo;
|
||||
|
||||
$this->statusInfo->setMessageRefIdentifier($info->imsx_messageIdentifier);
|
||||
|
||||
error_log("Service Request: tool version {$info->imsx_version} message ID {$info->imsx_messageIdentifier}");
|
||||
}
|
||||
|
||||
abstract protected function processBody();
|
||||
|
||||
/**
|
||||
* @return ImsLtiServiceResponse|null
|
||||
*/
|
||||
private function generateResponse()
|
||||
{
|
||||
$response = ImsLtiServiceResponseFactory::create(
|
||||
$this->responseType,
|
||||
$this->statusInfo,
|
||||
$this->responseBodyParam
|
||||
);
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
35
plugin/ims_lti/src/ImsLtiServiceRequestFactory.php
Normal file
35
plugin/ims_lti/src/ImsLtiServiceRequestFactory.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
/* For licensing terms, see /license.txt */
|
||||
|
||||
/**
|
||||
* Class ImsLtiServiceRequestFactory.
|
||||
*/
|
||||
class ImsLtiServiceRequestFactory
|
||||
{
|
||||
/**
|
||||
* @return ImsLtiServiceRequest|null
|
||||
*/
|
||||
public static function create(SimpleXMLElement $xml)
|
||||
{
|
||||
$bodyChildren = $xml->imsx_POXBody->children();
|
||||
|
||||
if (!empty($bodyChildren)) {
|
||||
$name = $bodyChildren->getName();
|
||||
|
||||
switch ($name) {
|
||||
case 'replaceResultRequest':
|
||||
return new ImsLtiServiceReplaceRequest($xml);
|
||||
case 'readResultRequest':
|
||||
return new ImsLtiServiceReadRequest($xml);
|
||||
case 'deleteResultRequest':
|
||||
return new ImsLtiServiceDeleteRequest($xml);
|
||||
default:
|
||||
$name = str_replace(['ResultRequest', 'Request'], '', $name);
|
||||
|
||||
return new ImsLtiServiceUnsupportedRequest($xml, $name);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
60
plugin/ims_lti/src/ImsLtiServiceResponse.php
Normal file
60
plugin/ims_lti/src/ImsLtiServiceResponse.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
/* For licensing terms, see /license.txt */
|
||||
|
||||
/**
|
||||
* Class ImsLtiServiceResponse.
|
||||
*/
|
||||
abstract class ImsLtiServiceResponse
|
||||
{
|
||||
const TYPE_REPLACE = 'replace';
|
||||
const TYPE_READ = 'read';
|
||||
const TYPE_DELETE = 'delete';
|
||||
|
||||
/**
|
||||
* @var mixed
|
||||
*/
|
||||
protected $bodyParams;
|
||||
/**
|
||||
* @var ImsLtiServiceResponseStatus
|
||||
*/
|
||||
private $statusInfo;
|
||||
|
||||
/**
|
||||
* ImsLtiServiceResponse constructor.
|
||||
*
|
||||
* @param mixed|null $bodyParam
|
||||
*/
|
||||
public function __construct(ImsLtiServiceResponseStatus $statusInfo, $bodyParam = null)
|
||||
{
|
||||
$this->statusInfo = $statusInfo;
|
||||
$this->bodyParams = $bodyParam;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
$xml = new SimpleXMLElement('<imsx_POXEnvelopeResponse></imsx_POXEnvelopeResponse>');
|
||||
$xml->addAttribute('xmlns', 'http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0');
|
||||
|
||||
$headerInfo = $xml->addChild('imsx_POXHeader')->addChild('imsx_POXResponseHeaderInfo');
|
||||
$headerInfo->addChild('imsx_version', 'V1.0');
|
||||
$headerInfo->addChild('imsx_messageIdentifier', time());
|
||||
|
||||
$statusInfo = $headerInfo->addChild('imsx_statusInfo');
|
||||
$statusInfo->addChild('imsx_codeMajor', $this->statusInfo->getCodeMajor());
|
||||
$statusInfo->addChild('imsx_severity', $this->statusInfo->getSeverity());
|
||||
$statusInfo->addChild('imsx_description', $this->statusInfo->getDescription());
|
||||
$statusInfo->addChild('imsx_messageRefIdentifier', $this->statusInfo->getMessageRefIdentifier());
|
||||
$statusInfo->addChild('imsx_operationRefIdentifier', $this->statusInfo->getOperationRefIdentifier());
|
||||
|
||||
$body = $xml->addChild('imsx_POXBody');
|
||||
|
||||
$this->generateBody($body);
|
||||
|
||||
return $xml->asXML();
|
||||
}
|
||||
|
||||
abstract protected function generateBody(SimpleXMLElement $xmlBody);
|
||||
}
|
||||
30
plugin/ims_lti/src/ImsLtiServiceResponseFactory.php
Normal file
30
plugin/ims_lti/src/ImsLtiServiceResponseFactory.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
/* For licensing terms, see /license.txt */
|
||||
|
||||
/**
|
||||
* Class ImsLtiServiceResponseFactory.
|
||||
*/
|
||||
class ImsLtiServiceResponseFactory
|
||||
{
|
||||
/**
|
||||
* @param string $type
|
||||
* @param mixed $bodyParam
|
||||
*
|
||||
* @return ImsLtiServiceResponse|null
|
||||
*/
|
||||
public static function create($type, ImsLtiServiceResponseStatus $statusInfo, $bodyParam = null)
|
||||
{
|
||||
switch ($type) {
|
||||
case ImsLtiServiceResponse::TYPE_REPLACE:
|
||||
return new ImsLtiServiceReplaceResponse($statusInfo, $bodyParam);
|
||||
case ImsLtiServiceResponse::TYPE_READ:
|
||||
return new ImsLtiServiceReadResponse($statusInfo, $bodyParam);
|
||||
case ImsLtiServiceResponse::TYPE_DELETE:
|
||||
return new ImsLtiServiceDeleteResponse($statusInfo, $bodyParam);
|
||||
default:
|
||||
return new ImsLtiServiceUnsupportedResponse($statusInfo, $type);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
162
plugin/ims_lti/src/ImsLtiServiceResponseStatus.php
Normal file
162
plugin/ims_lti/src/ImsLtiServiceResponseStatus.php
Normal file
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
/* For licensing terms, see /license.txt */
|
||||
|
||||
/**
|
||||
* Class ImsLtiResponseStatus.
|
||||
*/
|
||||
class ImsLtiServiceResponseStatus
|
||||
{
|
||||
const SEVERITY_STATUS = 'status';
|
||||
const SEVERITY_WARNING = 'warning';
|
||||
const SEVERITY_ERROR = 'error';
|
||||
|
||||
const CODEMAJOR_SUCCESS = 'success';
|
||||
const CODEMAJOR_PROCESSING = 'processing';
|
||||
const CODEMAJOR_FAILURE = 'failure';
|
||||
const CODEMAJOR_UNSUPPORTED = 'unsupported';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $codeMajor = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $severity = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $messageRefIdentifier = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $operationRefIdentifier = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $description = '';
|
||||
|
||||
/**
|
||||
* Get codeMajor.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCodeMajor()
|
||||
{
|
||||
return $this->codeMajor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set codeMajor.
|
||||
*
|
||||
* @param string $codeMajor
|
||||
*
|
||||
* @return ImsLtiServiceResponseStatus
|
||||
*/
|
||||
public function setCodeMajor($codeMajor)
|
||||
{
|
||||
$this->codeMajor = $codeMajor;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get severity.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getSeverity()
|
||||
{
|
||||
return $this->severity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set severity.
|
||||
*
|
||||
* @param string $severity
|
||||
*
|
||||
* @return ImsLtiServiceResponseStatus
|
||||
*/
|
||||
public function setSeverity($severity)
|
||||
{
|
||||
$this->severity = $severity;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get messageRefIdentifier.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getMessageRefIdentifier()
|
||||
{
|
||||
return $this->messageRefIdentifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set messageRefIdentifier.
|
||||
*
|
||||
* @param int $messageRefIdentifier
|
||||
*
|
||||
* @return ImsLtiServiceResponseStatus
|
||||
*/
|
||||
public function setMessageRefIdentifier($messageRefIdentifier)
|
||||
{
|
||||
$this->messageRefIdentifier = $messageRefIdentifier;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get operationRefIdentifier.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getOperationRefIdentifier()
|
||||
{
|
||||
return $this->operationRefIdentifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set operationRefIdentifier.
|
||||
*
|
||||
* @param int $operationRefIdentifier
|
||||
*
|
||||
* @return ImsLtiServiceResponseStatus
|
||||
*/
|
||||
public function setOperationRefIdentifier($operationRefIdentifier)
|
||||
{
|
||||
$this->operationRefIdentifier = $operationRefIdentifier;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get description.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set description.
|
||||
*
|
||||
* @param string $description
|
||||
*
|
||||
* @return ImsLtiServiceResponseStatus
|
||||
*/
|
||||
public function setDescription($description)
|
||||
{
|
||||
$this->description = $description;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
30
plugin/ims_lti/src/ImsLtiServiceUnsupportedRequest.php
Normal file
30
plugin/ims_lti/src/ImsLtiServiceUnsupportedRequest.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
/* For licensing terms, see /license.txt */
|
||||
|
||||
/**
|
||||
* Class ImsLtiServiceUnsupportedRequest.
|
||||
*/
|
||||
class ImsLtiServiceUnsupportedRequest extends ImsLtiServiceRequest
|
||||
{
|
||||
/**
|
||||
* ImsLtiDeleteServiceRequest constructor.
|
||||
*
|
||||
* @param string $name
|
||||
*/
|
||||
public function __construct(SimpleXMLElement $xml, $name)
|
||||
{
|
||||
parent::__construct($xml);
|
||||
|
||||
$this->responseType = $name;
|
||||
}
|
||||
|
||||
protected function processBody()
|
||||
{
|
||||
$this->statusInfo
|
||||
->setSeverity(ImsLtiServiceResponseStatus::SEVERITY_STATUS)
|
||||
->setCodeMajor(ImsLtiServiceResponseStatus::CODEMAJOR_UNSUPPORTED)
|
||||
->setDescription(
|
||||
$this->responseType.' is not supported'
|
||||
);
|
||||
}
|
||||
}
|
||||
24
plugin/ims_lti/src/ImsLtiServiceUnsupportedResponse.php
Normal file
24
plugin/ims_lti/src/ImsLtiServiceUnsupportedResponse.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
/* For licensing terms, see /license.txt */
|
||||
|
||||
/**
|
||||
* Class ImsLtiServiceUnsupportedResponse.
|
||||
*/
|
||||
class ImsLtiServiceUnsupportedResponse extends ImsLtiServiceResponse
|
||||
{
|
||||
/**
|
||||
* ImsLtiServiceUnsupportedResponse constructor.
|
||||
*
|
||||
* @param string $type
|
||||
*/
|
||||
public function __construct(ImsLtiServiceResponseStatus $statusInfo, $type)
|
||||
{
|
||||
$statusInfo->setOperationRefIdentifier($type);
|
||||
|
||||
parent::__construct($statusInfo);
|
||||
}
|
||||
|
||||
protected function generateBody(SimpleXMLElement $xmlBody)
|
||||
{
|
||||
}
|
||||
}
|
||||
149
plugin/ims_lti/src/LtiAuthException.php
Normal file
149
plugin/ims_lti/src/LtiAuthException.php
Normal file
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
/* For licensing terms, see /license.txt */
|
||||
|
||||
/**
|
||||
* Class LtiAuthException.
|
||||
*/
|
||||
class LtiAuthException extends Exception
|
||||
{
|
||||
const INVALID_REQUEST = 1;
|
||||
const INVALID_SCOPE = 2;
|
||||
const UNSUPPORTED_RESPONSE_TYPE = 3;
|
||||
const UNAUTHORIZED_CLIENT = 4;
|
||||
const ACCESS_DENIED = 5;
|
||||
const UNREGISTERED_REDIRECT_URI = 6;
|
||||
const INVALID_RESPONSE_MODE = 7;
|
||||
const MISSING_RESPONSE_MODE = 8;
|
||||
const INVALID_PROMPT = 9;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $type;
|
||||
|
||||
/**
|
||||
* LtiAuthException constructor.
|
||||
*
|
||||
* @param int $code
|
||||
*/
|
||||
public function __construct($code = 0, Throwable $previous = null)
|
||||
{
|
||||
switch ($code) {
|
||||
case self::INVALID_SCOPE:
|
||||
$this->type = 'invalid_scope';
|
||||
$message = 'Invalid scope';
|
||||
break;
|
||||
case self::UNSUPPORTED_RESPONSE_TYPE:
|
||||
$this->type = 'unsupported_response_type';
|
||||
$message = 'Unsupported responde type';
|
||||
break;
|
||||
case self::UNAUTHORIZED_CLIENT:
|
||||
$this->type = 'unauthorized_client';
|
||||
$message = 'Unauthorized client';
|
||||
break;
|
||||
case self::ACCESS_DENIED:
|
||||
$this->type = 'access_denied';
|
||||
$message = 'Access denied';
|
||||
break;
|
||||
case self::UNREGISTERED_REDIRECT_URI:
|
||||
$message = 'Unregistered redirect_uri';
|
||||
break;
|
||||
case self::INVALID_RESPONSE_MODE:
|
||||
$message = 'Invalid response_mode';
|
||||
break;
|
||||
case self::MISSING_RESPONSE_MODE:
|
||||
$message = 'Missing response_mode';
|
||||
break;
|
||||
case self::INVALID_PROMPT:
|
||||
$message = 'Invalid prompt';
|
||||
break;
|
||||
case self::INVALID_REQUEST:
|
||||
default:
|
||||
$this->type = 'invalid_request';
|
||||
$message = 'Invalid request';
|
||||
break;
|
||||
}
|
||||
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LtiAuthException
|
||||
*/
|
||||
public static function invalidRequest(Throwable $previous = null)
|
||||
{
|
||||
return new self(self::INVALID_REQUEST, $previous);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LtiAuthException
|
||||
*/
|
||||
public static function invalidScope(Throwable $previous = null)
|
||||
{
|
||||
return new self(self::INVALID_SCOPE, $previous);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LtiAuthException
|
||||
*/
|
||||
public static function unsupportedResponseType(Throwable $previous = null)
|
||||
{
|
||||
return new self(self::UNSUPPORTED_RESPONSE_TYPE, $previous);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LtiAuthException
|
||||
*/
|
||||
public static function unauthorizedClient(Throwable $previous = null)
|
||||
{
|
||||
return new self(self::UNAUTHORIZED_CLIENT, $previous);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LtiAuthException
|
||||
*/
|
||||
public static function accessDenied(Throwable $previous = null)
|
||||
{
|
||||
return new self(self::ACCESS_DENIED, $previous);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LtiAuthException
|
||||
*/
|
||||
public static function unregisteredRedirectUri(Throwable $previous = null)
|
||||
{
|
||||
return new self(self::UNREGISTERED_REDIRECT_URI, $previous);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LtiAuthException
|
||||
*/
|
||||
public static function invalidRespondeMode(Throwable $previous = null)
|
||||
{
|
||||
return new self(self::INVALID_RESPONSE_MODE, $previous);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LtiAuthException
|
||||
*/
|
||||
public static function missingResponseMode(Throwable $previous = null)
|
||||
{
|
||||
return new self(self::MISSING_RESPONSE_MODE, $previous);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LtiAuthException
|
||||
*/
|
||||
public static function invalidPrompt(Throwable $previous = null)
|
||||
{
|
||||
return new self(self::INVALID_PROMPT, $previous);
|
||||
}
|
||||
}
|
||||
117
plugin/ims_lti/src/Request/LtiTokenRequest.php
Normal file
117
plugin/ims_lti/src/Request/LtiTokenRequest.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
/* For licensing terms, see /license.txt */
|
||||
|
||||
use Chamilo\PluginBundle\Entity\ImsLti\ImsLtiTool;
|
||||
use Chamilo\PluginBundle\Entity\ImsLti\Token;
|
||||
use Firebase\JWT\JWT;
|
||||
|
||||
/**
|
||||
* Class LtiTokenRequest.
|
||||
*/
|
||||
class LtiTokenRequest
|
||||
{
|
||||
/**
|
||||
* @var ImsLtiTool
|
||||
*/
|
||||
private $tool;
|
||||
|
||||
/**
|
||||
* Validate the request's client assertion. Return the right tool.
|
||||
*
|
||||
* @param string $clientAssertion
|
||||
*
|
||||
* @throws Exception
|
||||
*
|
||||
* @return ImsLtiTool
|
||||
*/
|
||||
public function validateClientAssertion($clientAssertion)
|
||||
{
|
||||
$parts = explode('.', $clientAssertion);
|
||||
|
||||
if (count($parts) !== 3) {
|
||||
throw new Exception();
|
||||
}
|
||||
|
||||
$payload = JWT::urlsafeB64Decode($parts[1]);
|
||||
$claims = json_decode($payload, true);
|
||||
|
||||
if (empty($claims) || empty($claims['sub'])) {
|
||||
throw new Exception();
|
||||
}
|
||||
|
||||
$this->tool = Database::getManager()
|
||||
->getRepository('ChamiloPluginBundle:ImsLti\ImsLtiTool')
|
||||
->findOneBy(['clientId' => $claims['sub']]);
|
||||
|
||||
if (!$this->tool ||
|
||||
$this->tool->getVersion() !== ImsLti::V_1P3 ||
|
||||
empty($this->tool->publicKey)
|
||||
) {
|
||||
throw new Exception();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the request' scope. Return the allowed scopes in services.
|
||||
*
|
||||
* @param string $scope
|
||||
*
|
||||
* @throws Exception
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function validateScope($scope)
|
||||
{
|
||||
if (empty($scope)) {
|
||||
throw new Exception();
|
||||
}
|
||||
|
||||
$services = ImsLti::getAdvantageServices($this->tool);
|
||||
|
||||
$requested = explode(' ', $scope);
|
||||
$allowed = [];
|
||||
|
||||
/** @var LtiAdvantageService $service */
|
||||
foreach ($services as $service) {
|
||||
$allowed = array_merge($allowed, $service->getAllowedScopes());
|
||||
}
|
||||
|
||||
$intersect = array_intersect($requested, $allowed);
|
||||
|
||||
if (empty($intersect)) {
|
||||
throw new Exception();
|
||||
}
|
||||
|
||||
return $intersect;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $clientAssertion
|
||||
*
|
||||
* @throws Exception
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
public function decodeJwt($clientAssertion)
|
||||
{
|
||||
return JWT::decode($clientAssertion, $this->tool->publicKey, ['RS256']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Token
|
||||
*/
|
||||
public function generateToken(array $allowedScopes)
|
||||
{
|
||||
$now = api_get_utc_datetime(null, false, true)->getTimestamp();
|
||||
|
||||
$token = new Token();
|
||||
$token
|
||||
->generateHash()
|
||||
->setTool($this->tool)
|
||||
->setScope($allowedScopes)
|
||||
->setCreatedAt($now)
|
||||
->setExpiresAt($now + Token::TOKEN_LIFETIME);
|
||||
|
||||
return $token;
|
||||
}
|
||||
}
|
||||
49
plugin/ims_lti/src/Service/LtiAdvantageService.php
Normal file
49
plugin/ims_lti/src/Service/LtiAdvantageService.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
/* For licensing terms, see /license.txt */
|
||||
|
||||
use Chamilo\PluginBundle\Entity\ImsLti\ImsLtiTool;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
/**
|
||||
* Class LtiAdvantageService.
|
||||
*/
|
||||
abstract class LtiAdvantageService
|
||||
{
|
||||
/**
|
||||
* @var ImsLtiTool
|
||||
*/
|
||||
protected $tool;
|
||||
|
||||
/**
|
||||
* LtiAdvantageService constructor.
|
||||
*/
|
||||
public function __construct(ImsLtiTool $tool)
|
||||
{
|
||||
$this->tool = $tool;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LtiAdvantageService
|
||||
*/
|
||||
public function setTool(ImsLtiTool $tool)
|
||||
{
|
||||
$this->tool = $tool;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
abstract public function getAllowedScopes();
|
||||
|
||||
/**
|
||||
* @throws \Doctrine\ORM\ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
* @throws \Doctrine\ORM\TransactionRequiredException
|
||||
*
|
||||
* @return LtiServiceResource
|
||||
*/
|
||||
abstract public static function getResource(Request $request, JsonResponse $response);
|
||||
}
|
||||
155
plugin/ims_lti/src/Service/LtiAssignmentGradesService.php
Normal file
155
plugin/ims_lti/src/Service/LtiAssignmentGradesService.php
Normal file
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
/* For licensing terms, see /license.txt */
|
||||
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
/**
|
||||
* Class LtiAssignmentGradesService.
|
||||
*/
|
||||
class LtiAssignmentGradesService extends LtiAdvantageService
|
||||
{
|
||||
const AGS_NONE = 'none';
|
||||
const AGS_SIMPLE = 'simple';
|
||||
const AGS_FULL = 'full';
|
||||
|
||||
const SCOPE_LINE_ITEM = 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem';
|
||||
const SCOPE_LINE_ITEM_READ = 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly';
|
||||
const SCOPE_RESULT_READ = 'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly';
|
||||
const SCOPE_SCORE_WRITE = 'https://purl.imsglobal.org/spec/lti-ags/scope/score';
|
||||
|
||||
const TYPE_LINE_ITEM_CONTAINER = 'application/vnd.ims.lis.v2.lineitemcontainer+json';
|
||||
const TYPE_LINE_ITEM = 'application/vnd.ims.lis.v2.lineitem+json';
|
||||
const TYPE_RESULT_CONTAINER = 'application/vnd.ims.lis.v2.resultcontainer+json';
|
||||
const TYPE_SCORE = 'application/vnd.ims.lis.v1.score+json';
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getAllowedScopes()
|
||||
{
|
||||
$scopes = [
|
||||
self::SCOPE_LINE_ITEM_READ,
|
||||
self::SCOPE_RESULT_READ,
|
||||
self::SCOPE_SCORE_WRITE,
|
||||
];
|
||||
|
||||
$toolServices = $this->tool->getAdvantageServices();
|
||||
|
||||
if (self::AGS_FULL === $toolServices['ags']) {
|
||||
$scopes[] = self::SCOPE_LINE_ITEM;
|
||||
}
|
||||
|
||||
return $scopes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Doctrine\ORM\ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
* @throws \Doctrine\ORM\TransactionRequiredException
|
||||
*
|
||||
* @return LtiAdvantageServiceResource
|
||||
*/
|
||||
public static function getResource(Request $request, JsonResponse $response)
|
||||
{
|
||||
$parts = explode('/', $request->getPathInfo());
|
||||
$parts = array_filter($parts);
|
||||
|
||||
$resource = null;
|
||||
|
||||
if (count($parts) === 2 && 'lineitems' === $parts[2]) {
|
||||
$resource = new LtiLineItemsResource(
|
||||
$request->query->get('t'),
|
||||
$parts[1]
|
||||
);
|
||||
}
|
||||
|
||||
if (count($parts) === 3 && 'lineitems' === $parts[2]) {
|
||||
$resource = new LtiLineItemResource(
|
||||
$request->query->get('t'),
|
||||
$parts[1],
|
||||
$parts[3]
|
||||
);
|
||||
}
|
||||
|
||||
if (isset($parts[4]) && 'results' === $parts[4]) {
|
||||
$resource = new LtiResultsResource(
|
||||
$request->query->get('t'),
|
||||
$parts[1],
|
||||
$parts[3]
|
||||
);
|
||||
}
|
||||
|
||||
if (isset($parts[4]) && 'scores' === $parts[4]) {
|
||||
$resource = new LtiScoresResource(
|
||||
$request->query->get('t'),
|
||||
$parts[1],
|
||||
$parts[3]
|
||||
);
|
||||
}
|
||||
|
||||
if (!$resource) {
|
||||
throw new NotFoundHttpException('Line item resource not found.');
|
||||
}
|
||||
|
||||
return $resource
|
||||
->setRequest($request)
|
||||
->setResponse($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $contextId
|
||||
* @param int $toolId
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getLineItemsUrl($contextId, $toolId, array $extraParams = [])
|
||||
{
|
||||
$base = api_get_path(WEB_PLUGIN_PATH).'ims_lti/ags2.php';
|
||||
$resource = str_replace(
|
||||
'context_id',
|
||||
$contextId,
|
||||
LtiLineItemsResource::URL_TEMPLATE
|
||||
);
|
||||
$params = array_merge($extraParams, ['t' => $toolId]);
|
||||
$query = http_build_query($params);
|
||||
|
||||
return "$base$resource?$query";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $contextId
|
||||
* @param int $lineItemId
|
||||
* @param int $toolId
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getLineItemUrl($contextId, $lineItemId, $toolId)
|
||||
{
|
||||
$base = api_get_path(WEB_PLUGIN_PATH).'ims_lti/ags2.php';
|
||||
$resource = str_replace(
|
||||
['context_id', 'line_item_id'],
|
||||
[$contextId, $lineItemId],
|
||||
LtiLineItemResource::URL_TEMPLATE
|
||||
);
|
||||
$query = http_build_query(['t' => $toolId]);
|
||||
|
||||
return "$base$resource?$query";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $contextId
|
||||
* @param int $lineItemId
|
||||
* @param int $toolId
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getResultsUrl($contextId, $lineItemId, $toolId, array $extraParams = [])
|
||||
{
|
||||
$lineItemUrl = self::getLineItemUrl($contextId, $lineItemId, $toolId);
|
||||
$query = http_build_query($extraParams);
|
||||
|
||||
return "$lineItemUrl/results?$query";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
/* For licensing terms, see /license.txt */
|
||||
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
/**
|
||||
* Class LtiNamesRoleProvisioningService.
|
||||
*/
|
||||
class LtiNamesRoleProvisioningService extends LtiAdvantageService
|
||||
{
|
||||
const NRPS_NONE = 'none';
|
||||
const NRPS_CONTEXT_MEMBERSHIP = 'simple';
|
||||
|
||||
const SCOPE_CONTEXT_MEMBERSHIP_READ = 'https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly';
|
||||
|
||||
const TYPE_MEMBERSHIP_CONTAINER = 'application/vnd.ims.lti-nrps.v2.membershipcontainer+json';
|
||||
|
||||
const USER_STATUS_ACTIVE = 'Active';
|
||||
const USER_STATUS_INACTIVE = 'Inactive';
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getAllowedScopes()
|
||||
{
|
||||
return [
|
||||
self::SCOPE_CONTEXT_MEMBERSHIP_READ,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public static function getResource(Request $request, JsonResponse $response)
|
||||
{
|
||||
$parts = explode('/', $request->getPathInfo());
|
||||
$parts = array_filter($parts);
|
||||
|
||||
$resource = null;
|
||||
|
||||
if (isset($parts[1], $parts[2]) &&
|
||||
(int) $parts[1] > 0 && 'memberships' === $parts[2]
|
||||
) {
|
||||
$resource = new LtiContextMembershipResource(
|
||||
$request->query->getInt('t'),
|
||||
$parts[1],
|
||||
$request->query->getInt('s')
|
||||
);
|
||||
}
|
||||
|
||||
if (!$resource) {
|
||||
throw new NotFoundHttpException('Resource not found for Name and Role Provisioning.');
|
||||
}
|
||||
|
||||
return $resource
|
||||
->setRequest($request)
|
||||
->setResponse($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $toolId
|
||||
* @param int $courseId
|
||||
* @param int $sessionId
|
||||
* @param array $extraParams
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getUrl($toolId, $courseId, $sessionId = 0, $extraParams = [])
|
||||
{
|
||||
$base = api_get_path(WEB_PLUGIN_PATH).'ims_lti/nrps2.php';
|
||||
$resource = str_replace(
|
||||
'context_id',
|
||||
$courseId,
|
||||
LtiContextMembershipResource::URL_TEMPLATE
|
||||
);
|
||||
$query = http_build_query(['s' => $sessionId, 't' => $toolId]);
|
||||
|
||||
if ($extraParams) {
|
||||
$query .= '&'.http_build_query($extraParams);
|
||||
}
|
||||
|
||||
return "$base$resource?$query";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
/* For licensing terms, see /license.txt */
|
||||
|
||||
use Chamilo\CoreBundle\Entity\Course;
|
||||
use Chamilo\PluginBundle\Entity\ImsLti\ImsLtiTool;
|
||||
use Chamilo\PluginBundle\Entity\ImsLti\Token;
|
||||
use Doctrine\ORM\OptimisticLockException;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Doctrine\ORM\TransactionRequiredException;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
|
||||
|
||||
/**
|
||||
* Class LtiAdvantageServiceResource.
|
||||
*/
|
||||
abstract class LtiAdvantageServiceResource
|
||||
{
|
||||
const URL_TEMPLATE = '/';
|
||||
|
||||
/**
|
||||
* @var Request
|
||||
*/
|
||||
protected $request;
|
||||
/**
|
||||
* @var JsonResponse
|
||||
*/
|
||||
protected $response;
|
||||
/**
|
||||
* @var Course
|
||||
*/
|
||||
protected $course;
|
||||
/**
|
||||
* @var ImsLtiTool
|
||||
*/
|
||||
protected $tool;
|
||||
|
||||
/**
|
||||
* LtiAdvantageServiceResource constructor.
|
||||
*
|
||||
* @param int $toolId
|
||||
* @param int $courseId
|
||||
*
|
||||
* @throws ORMException
|
||||
* @throws OptimisticLockException
|
||||
* @throws TransactionRequiredException
|
||||
*/
|
||||
public function __construct($toolId, $courseId)
|
||||
{
|
||||
$this->course = api_get_course_entity((int) $courseId);
|
||||
$this->tool = Database::getManager()->find('ChamiloPluginBundle:ImsLti\ImsLtiTool', (int) $toolId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LtiAdvantageServiceResource
|
||||
*/
|
||||
public function setRequest(Request $request)
|
||||
{
|
||||
$this->request = $request;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LtiAdvantageServiceResource
|
||||
*/
|
||||
public function setResponse(JsonResponse $response)
|
||||
{
|
||||
$this->response = $response;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws HttpExceptionInterface
|
||||
*/
|
||||
abstract public function validate();
|
||||
|
||||
/**
|
||||
* @throws MethodNotAllowedHttpException
|
||||
*/
|
||||
abstract public function process();
|
||||
|
||||
/**
|
||||
* @throws HttpException
|
||||
*/
|
||||
protected function validateToken(array $allowedScopes)
|
||||
{
|
||||
$headers = getallheaders();
|
||||
$authorization = isset($headers['Authorization']) ? $headers['Authorization'] : '';
|
||||
|
||||
if (substr($authorization, 0, 7) !== 'Bearer ') {
|
||||
throw new BadRequestHttpException('Authorization is missing.');
|
||||
}
|
||||
|
||||
$hash = trim(substr($authorization, 7));
|
||||
|
||||
/** @var Token $token */
|
||||
$token = Database::getManager()
|
||||
->getRepository('ChamiloPluginBundle:ImsLti\Token')
|
||||
->findOneBy(['hash' => $hash]);
|
||||
|
||||
if (!$token) {
|
||||
throw new BadRequestHttpException('Authorization token invalid.');
|
||||
}
|
||||
|
||||
if ($token->getExpiresAt() < api_get_utc_datetime(null, false, true)->getTimestamp()) {
|
||||
throw new BadRequestHttpException('Authorization token expired.');
|
||||
}
|
||||
|
||||
$intersect = array_intersect(
|
||||
$token->getScope(),
|
||||
$allowedScopes
|
||||
);
|
||||
|
||||
if (empty($intersect)) {
|
||||
throw new BadRequestHttpException('Authorization token invalid for the scope.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
<?php
|
||||
/* For licensing terms, see /license.txt */
|
||||
|
||||
use Chamilo\CoreBundle\Entity\CourseRelUser;
|
||||
use Chamilo\CoreBundle\Entity\Session;
|
||||
use Chamilo\CoreBundle\Entity\SessionRelCourseRelUser;
|
||||
use Chamilo\UserBundle\Entity\User;
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
|
||||
|
||||
/**
|
||||
* Class LtiContextMembershipResource.
|
||||
*/
|
||||
class LtiContextMembershipResource extends LtiAdvantageServiceResource
|
||||
{
|
||||
const URL_TEMPLATE = '/context_id/memberships';
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $inSession = false;
|
||||
/**
|
||||
* @var Session
|
||||
*/
|
||||
private $session;
|
||||
|
||||
/**
|
||||
* LtiContextMembershipResorce constructor.
|
||||
*
|
||||
* @param int $toolId
|
||||
* @param int $courseId
|
||||
* @param int $sessionId
|
||||
*
|
||||
* @throws \Doctrine\ORM\ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
* @throws \Doctrine\ORM\TransactionRequiredException
|
||||
*/
|
||||
public function __construct($toolId, $courseId, $sessionId = 0)
|
||||
{
|
||||
$this->session = api_get_session_entity($sessionId);
|
||||
|
||||
parent::__construct($toolId, $courseId);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function validate()
|
||||
{
|
||||
if ($this->request->server->get('HTTP_ACCEPT') !== LtiNamesRoleProvisioningService::TYPE_MEMBERSHIP_CONTAINER) {
|
||||
throw new UnsupportedMediaTypeHttpException('Unsupported media type.');
|
||||
}
|
||||
|
||||
if (!$this->course) {
|
||||
throw new BadRequestHttpException('Course not found.');
|
||||
}
|
||||
|
||||
if (!$this->tool) {
|
||||
throw new BadRequestHttpException('Tool not found.');
|
||||
}
|
||||
|
||||
if (null === $this->tool->getCourse()) {
|
||||
throw new BadRequestHttpException('Tool not enabled.');
|
||||
}
|
||||
|
||||
if ($this->tool->getCourse()->getId() !== $this->course->getId()) {
|
||||
throw new AccessDeniedHttpException('Tool not found in course.');
|
||||
}
|
||||
|
||||
$sessionId = (int) $this->request->query->get('s');
|
||||
$this->inSession = $sessionId > 0;
|
||||
|
||||
if ($this->inSession && !$this->session) {
|
||||
throw new BadRequestHttpException('Session not found');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function process()
|
||||
{
|
||||
switch ($this->request->getMethod()) {
|
||||
case Request::METHOD_GET:
|
||||
$this->validateToken(
|
||||
[LtiNamesRoleProvisioningService::SCOPE_CONTEXT_MEMBERSHIP_READ]
|
||||
);
|
||||
$this->processGet();
|
||||
break;
|
||||
default:
|
||||
throw new MethodNotAllowedHttpException([Request::METHOD_GET]);
|
||||
}
|
||||
}
|
||||
|
||||
private function processGet()
|
||||
{
|
||||
$role = str_replace(
|
||||
'http://purl.imsglobal.org/vocab/lis/v2/membership#',
|
||||
'',
|
||||
$this->request->query->get('role')
|
||||
);
|
||||
$limit = $this->request->query->getInt('limit');
|
||||
$page = $this->request->query->getInt('page');
|
||||
$status = -1;
|
||||
|
||||
if ('Instructor' === $role) {
|
||||
$status = $this->session ? Session::COACH : User::COURSE_MANAGER;
|
||||
} elseif ('Learner' === $role) {
|
||||
$status = $this->session ? Session::STUDENT : User::STUDENT;
|
||||
}
|
||||
|
||||
$members = $this->getMembers($status, $limit, $page);
|
||||
|
||||
$data = $this->getGetData($members);
|
||||
|
||||
$this->setLinkHeaderToGet($status, $limit, $page);
|
||||
|
||||
$this->response->headers->set('Content-Type', LtiNamesRoleProvisioningService::TYPE_MEMBERSHIP_CONTAINER);
|
||||
$this->response->setData($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $status If $status = -1 then get all statuses.
|
||||
* @param int $limit
|
||||
* @param int $page
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getMembers($status, $limit, $page = 0)
|
||||
{
|
||||
if ($this->session) {
|
||||
$subscriptions = $this->session->getUsersSubscriptionsInCourse($this->course);
|
||||
|
||||
// Add session admin as teacher in course
|
||||
$adminSubscription = new SessionRelCourseRelUser();
|
||||
$adminSubscription->setCourse($this->course);
|
||||
$adminSubscription->setSession($this->session);
|
||||
$adminSubscription->setStatus(Session::COACH);
|
||||
$adminSubscription->setUser(
|
||||
api_get_user_entity($this->session->getSessionAdminId())
|
||||
);
|
||||
|
||||
$subscriptions->add($adminSubscription);
|
||||
} else {
|
||||
$subscriptions = $this->course->getUsers();
|
||||
}
|
||||
|
||||
$criteria = Criteria::create();
|
||||
|
||||
if ($status > -1) {
|
||||
$criteria->where(
|
||||
Criteria::expr()->eq('status', $status)
|
||||
);
|
||||
}
|
||||
|
||||
if ($limit > 0) {
|
||||
$criteria->setMaxResults($limit);
|
||||
|
||||
if ($page > 0) {
|
||||
$criteria->setFirstResult($page * $limit);
|
||||
}
|
||||
}
|
||||
|
||||
return $subscriptions->matching($criteria)->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
private function getGetData(array $members)
|
||||
{
|
||||
$platformDomain = str_replace(['https://', 'http://'], '', api_get_setting('InstitutionUrl'));
|
||||
$dataMembers = [];
|
||||
|
||||
$isSharingName = $this->tool->isSharingName();
|
||||
$isSharingEmail = $this->tool->isSharingEmail();
|
||||
$isSharingPicture = $this->tool->isSharingPicture();
|
||||
|
||||
foreach ($members as $member) {
|
||||
/** @var User $user */
|
||||
$user = $member->getUser();
|
||||
|
||||
$dataMember = [
|
||||
'status' => $user->isActive()
|
||||
? LtiNamesRoleProvisioningService::USER_STATUS_ACTIVE
|
||||
: LtiNamesRoleProvisioningService::USER_STATUS_INACTIVE,
|
||||
'user_id' => ImsLtiPlugin::getLaunchUserIdClaim($this->tool, $user),
|
||||
'lis_person_sourcedid' => ImsLti::getPersonSourcedId($platformDomain, $user),
|
||||
'lti11_legacy_user_id' => ImsLtiPlugin::generateToolUserId($user->getId()),
|
||||
];
|
||||
|
||||
if ($isSharingName) {
|
||||
$dataMember['name'] = $user->getFullname();
|
||||
$dataMember['given_name'] = $user->getFirstname();
|
||||
$dataMember['family_name'] = $user->getLastname();
|
||||
}
|
||||
|
||||
if ($isSharingEmail) {
|
||||
$dataMember['email'] = $user->getEmail();
|
||||
}
|
||||
|
||||
if ($isSharingPicture) {
|
||||
$dataMember['picture'] = UserManager::getUserPicture($user->getId());
|
||||
}
|
||||
|
||||
if ($member instanceof CourseRelUser) {
|
||||
$dataMember['roles'] = $member->getStatus() === User::COURSE_MANAGER
|
||||
? ['http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor']
|
||||
: ['http://purl.imsglobal.org/vocab/lis/v2/membership#Learner'];
|
||||
} elseif ($member instanceof SessionRelCourseRelUser) {
|
||||
$dataMember['roles'] = $member->getStatus() === Session::STUDENT
|
||||
? ['http://purl.imsglobal.org/vocab/lis/v2/membership#Learner']
|
||||
: ['http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor'];
|
||||
}
|
||||
|
||||
$dataMembers[] = $dataMember;
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => api_get_path(WEB_PLUGIN_PATH)
|
||||
."ims_lti/nrps2.php/{$this->course->getId()}/memberships?"
|
||||
.http_build_query(
|
||||
[
|
||||
't' => $this->tool->getId(),
|
||||
's' => $this->session ? $this->session->getId() : null,
|
||||
]
|
||||
),
|
||||
'context' => [
|
||||
'id' => (string) $this->course->getId(),
|
||||
'label' => $this->course->getCode(),
|
||||
'title' => $this->course->getTitle(),
|
||||
],
|
||||
'members' => $dataMembers,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $status
|
||||
* @param int $limit
|
||||
* @param int $page
|
||||
*/
|
||||
private function setLinkHeaderToGet($status, $limit, $page = 0)
|
||||
{
|
||||
if (!$limit) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->session) {
|
||||
$subscriptions = $this->session->getUsersSubscriptionsInCourse($this->course);
|
||||
} else {
|
||||
$subscriptions = $this->course->getUsers();
|
||||
}
|
||||
|
||||
$criteria = Criteria::create();
|
||||
|
||||
if ($status > -1) {
|
||||
$criteria->where(
|
||||
Criteria::expr()->eq('status', $status)
|
||||
);
|
||||
}
|
||||
|
||||
$count = $subscriptions->matching($criteria)->count();
|
||||
|
||||
if ($this->session) {
|
||||
// +1 for session admin
|
||||
$count++;
|
||||
}
|
||||
|
||||
if ($page + 1 < ceil($count / $limit)) {
|
||||
$url = LtiNamesRoleProvisioningService::getUrl(
|
||||
$this->tool->getId(),
|
||||
$this->course->getId(),
|
||||
$this->session ? $this->session->getId() : 0,
|
||||
[
|
||||
'role' => $this->request->query->get('role'),
|
||||
'limit' => $limit,
|
||||
'page' => $page + 1,
|
||||
]
|
||||
);
|
||||
|
||||
$this->response->headers->set(
|
||||
'Link',
|
||||
'<'.$url.'>; rel="next"'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
230
plugin/ims_lti/src/Service/Resource/LtiLineItemResource.php
Normal file
230
plugin/ims_lti/src/Service/Resource/LtiLineItemResource.php
Normal file
@@ -0,0 +1,230 @@
|
||||
<?php
|
||||
/* For licensing terms, see /license.txt */
|
||||
|
||||
use Chamilo\PluginBundle\Entity\ImsLti\LineItem;
|
||||
use Doctrine\ORM\OptimisticLockException;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Doctrine\ORM\TransactionRequiredException;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
|
||||
|
||||
/**
|
||||
* Class LtiLineItemResource.
|
||||
*/
|
||||
class LtiLineItemResource extends LtiAdvantageServiceResource
|
||||
{
|
||||
const URL_TEMPLATE = '/context_id/lineitems/line_item_id';
|
||||
|
||||
/**
|
||||
* @var LineItem|null
|
||||
*/
|
||||
private $lineItem;
|
||||
|
||||
/**
|
||||
* LtiLineItemResource constructor.
|
||||
*
|
||||
* @param int $toolId
|
||||
* @param int $courseId
|
||||
* @param int $lineItemId
|
||||
*
|
||||
* @throws ORMException
|
||||
* @throws OptimisticLockException
|
||||
* @throws TransactionRequiredException
|
||||
*/
|
||||
public function __construct($toolId, $courseId, $lineItemId)
|
||||
{
|
||||
parent::__construct($toolId, $courseId);
|
||||
|
||||
$this->lineItem = Database::getManager()->find('ChamiloPluginBundle:ImsLti\LineItem', (int) $lineItemId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws OptimisticLockException
|
||||
*/
|
||||
public function process()
|
||||
{
|
||||
switch ($this->request->getMethod()) {
|
||||
case Request::METHOD_GET:
|
||||
$this->validateToken(
|
||||
[LtiAssignmentGradesService::SCOPE_LINE_ITEM]
|
||||
);
|
||||
$this->processGet();
|
||||
break;
|
||||
case Request::METHOD_PUT:
|
||||
if (LtiAssignmentGradesService::AGS_FULL !== $this->tool->getAdvantageServices()['ags']) {
|
||||
throw new MethodNotAllowedHttpException([Request::METHOD_GET]);
|
||||
}
|
||||
|
||||
$this->validateToken(
|
||||
[LtiAssignmentGradesService::SCOPE_LINE_ITEM]
|
||||
);
|
||||
$this->processPut();
|
||||
break;
|
||||
case Request::METHOD_DELETE:
|
||||
if (LtiAssignmentGradesService::AGS_FULL !== $this->tool->getAdvantageServices()['ags']) {
|
||||
throw new MethodNotAllowedHttpException([Request::METHOD_GET]);
|
||||
}
|
||||
|
||||
$this->validateToken(
|
||||
[LtiAssignmentGradesService::SCOPE_LINE_ITEM]
|
||||
);
|
||||
$this->processDelete();
|
||||
break;
|
||||
default:
|
||||
throw new MethodNotAllowedHttpException([Request::METHOD_GET, Request::METHOD_PUT, Request::METHOD_DELETE]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the values for the resource URL.
|
||||
*/
|
||||
public function validate()
|
||||
{
|
||||
if (!$this->course) {
|
||||
throw new BadRequestHttpException('Course not found.');
|
||||
}
|
||||
|
||||
if (!$this->tool) {
|
||||
throw new BadRequestHttpException('Tool not found.');
|
||||
}
|
||||
|
||||
if ($this->tool->getCourse()->getId() !== $this->course->getId()) {
|
||||
throw new AccessDeniedHttpException('Tool not found in course.');
|
||||
}
|
||||
|
||||
if ($this->request->server->get('HTTP_ACCEPT') !== LtiAssignmentGradesService::TYPE_LINE_ITEM) {
|
||||
throw new UnsupportedMediaTypeHttpException('Unsupported media type.');
|
||||
}
|
||||
|
||||
$parentTool = $this->tool->getParent();
|
||||
|
||||
if ($parentTool) {
|
||||
$advServices = $parentTool->getAdvantageServices();
|
||||
|
||||
if (LtiAssignmentGradesService::AGS_NONE === $advServices['ags']) {
|
||||
throw new AccessDeniedHttpException('Assigment and grade service is not enabled for this tool.');
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->lineItem) {
|
||||
throw new NotFoundHttpException('Line item not found');
|
||||
}
|
||||
|
||||
if ($this->lineItem->getTool()->getId() !== $this->tool->getId()) {
|
||||
throw new AccessDeniedHttpException('Line item not found for the tool.');
|
||||
}
|
||||
}
|
||||
|
||||
private function processGet()
|
||||
{
|
||||
$data = $this->lineItem->toArray();
|
||||
$data['id'] = LtiAssignmentGradesService::getLineItemUrl(
|
||||
$this->course->getId(),
|
||||
$this->lineItem->getId(),
|
||||
$this->tool->getId()
|
||||
);
|
||||
|
||||
$this->response->headers->set('Content-Type', LtiAssignmentGradesService::TYPE_LINE_ITEM);
|
||||
$this->response->setData($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws OptimisticLockException
|
||||
*/
|
||||
private function processPut()
|
||||
{
|
||||
$data = json_decode($this->request->getContent(), true);
|
||||
|
||||
if (empty($data) || empty($data['label']) || empty($data['scoreMaximum'])) {
|
||||
throw new BadRequestHttpException('Missing data to update line item.');
|
||||
}
|
||||
|
||||
$this->updateLineItem($data);
|
||||
|
||||
$data['id'] = LtiAssignmentGradesService::getLineItemUrl(
|
||||
$this->course->getId(),
|
||||
$this->lineItem->getId(),
|
||||
$this->tool->getId()
|
||||
);
|
||||
$data['scoreMaximum'] = $this->lineItem->getEvaluation()->getMax();
|
||||
|
||||
$this->response->headers->set('Content-Type', LtiAssignmentGradesService::TYPE_LINE_ITEM);
|
||||
$this->response->setEncodingOptions(JSON_UNESCAPED_SLASHES);
|
||||
$this->response->setData($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws OptimisticLockException
|
||||
*/
|
||||
private function updateLineItem(array $data)
|
||||
{
|
||||
$lineItemEvaluation = $this->lineItem->getEvaluation();
|
||||
$evaluations = Evaluation::load($lineItemEvaluation->getId());
|
||||
/** @var Evaluation $evaluation */
|
||||
$evaluation = $evaluations[0];
|
||||
|
||||
$lineItemEvaluation->setName($data['label']);
|
||||
|
||||
if (isset($data['resourceId'])) {
|
||||
$this->lineItem->setResourceId($data['resourceId']);
|
||||
}
|
||||
|
||||
if (isset($data['tag'])) {
|
||||
$this->lineItem->setTag($data['tag']);
|
||||
}
|
||||
|
||||
if (!empty($data['startDateTime'])) {
|
||||
$startDate = new DateTime($data['startDateTime']);
|
||||
$this->lineItem->setStartDate($startDate);
|
||||
}
|
||||
|
||||
if (!empty($data['endDateTime'])) {
|
||||
$endDate = new DateTime($data['endDateTime']);
|
||||
$this->lineItem->setEndDate($endDate);
|
||||
}
|
||||
|
||||
if (!$evaluation->has_results()) {
|
||||
$lineItemEvaluation->setMax($data['scoreMaximum']);
|
||||
}
|
||||
|
||||
$em = Database::getManager();
|
||||
$em->persist($this->lineItem);
|
||||
$em->persist($lineItemEvaluation);
|
||||
|
||||
$em->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws OptimisticLockException
|
||||
*/
|
||||
private function processDelete()
|
||||
{
|
||||
$this->deleteLineItem();
|
||||
|
||||
$this->response->setStatusCode(Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws OptimisticLockException
|
||||
*/
|
||||
private function deleteLineItem()
|
||||
{
|
||||
$lineItemEvaluation = $this->lineItem->getEvaluation();
|
||||
$evaluations = Evaluation::load($lineItemEvaluation->getId());
|
||||
|
||||
/** @var Evaluation $evaluation */
|
||||
$evaluation = $evaluations[0];
|
||||
|
||||
$em = Database::getManager();
|
||||
|
||||
$em->remove($this->lineItem);
|
||||
$em->flush();
|
||||
|
||||
$evaluation->delete_with_results();
|
||||
}
|
||||
}
|
||||
265
plugin/ims_lti/src/Service/Resource/LtiLineItemsResource.php
Normal file
265
plugin/ims_lti/src/Service/Resource/LtiLineItemsResource.php
Normal file
@@ -0,0 +1,265 @@
|
||||
<?php
|
||||
/* For licensing terms, see /license.txt */
|
||||
|
||||
use Chamilo\PluginBundle\Entity\ImsLti\LineItem;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\ORM\OptimisticLockException;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Doctrine\ORM\TransactionRequiredException;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
|
||||
|
||||
/**
|
||||
* Class LtiLineItemsResource.
|
||||
*/
|
||||
class LtiLineItemsResource extends LtiAdvantageServiceResource
|
||||
{
|
||||
const URL_TEMPLATE = '/context_id/lineitems';
|
||||
|
||||
public function validate()
|
||||
{
|
||||
if (!$this->course) {
|
||||
throw new BadRequestHttpException('Course not found.');
|
||||
}
|
||||
|
||||
if (!$this->tool) {
|
||||
throw new BadRequestHttpException('Tool not found.');
|
||||
}
|
||||
|
||||
if ($this->tool->getCourse()->getId() !== $this->course->getId()) {
|
||||
throw new AccessDeniedHttpException('Tool not found in course.');
|
||||
}
|
||||
|
||||
$isMediaTypeAllowed = in_array(
|
||||
$this->request->server->get('HTTP_ACCEPT'),
|
||||
[
|
||||
LtiAssignmentGradesService::TYPE_LINE_ITEM_CONTAINER,
|
||||
LtiAssignmentGradesService::TYPE_LINE_ITEM,
|
||||
]
|
||||
);
|
||||
|
||||
if (!$isMediaTypeAllowed) {
|
||||
throw new UnsupportedMediaTypeHttpException('Unsupported media type.');
|
||||
}
|
||||
|
||||
$parentTool = $this->tool->getParent();
|
||||
|
||||
if ($parentTool) {
|
||||
$advServices = $parentTool->getAdvantageServices();
|
||||
|
||||
if (LtiAssignmentGradesService::AGS_NONE === $advServices['ags']) {
|
||||
throw new AccessDeniedHttpException('Assigment and grade service is not enabled for this tool.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ORMException
|
||||
* @throws OptimisticLockException
|
||||
* @throws TransactionRequiredException
|
||||
* @throws MethodNotAllowedHttpException
|
||||
*/
|
||||
public function process()
|
||||
{
|
||||
switch ($this->request->getMethod()) {
|
||||
case Request::METHOD_POST:
|
||||
if (LtiAssignmentGradesService::AGS_FULL !== $this->tool->getAdvantageServices()['ags']) {
|
||||
throw new MethodNotAllowedHttpException([Request::METHOD_GET]);
|
||||
}
|
||||
|
||||
$this->validateToken(
|
||||
[
|
||||
LtiAssignmentGradesService::SCOPE_LINE_ITEM,
|
||||
]
|
||||
);
|
||||
$this->processPost();
|
||||
break;
|
||||
case Request::METHOD_GET:
|
||||
$this->validateToken(
|
||||
[
|
||||
LtiAssignmentGradesService::SCOPE_LINE_ITEM,
|
||||
LtiAssignmentGradesService::SCOPE_LINE_ITEM_READ,
|
||||
]
|
||||
);
|
||||
$this->processGet();
|
||||
break;
|
||||
default:
|
||||
throw new MethodNotAllowedHttpException([Request::METHOD_GET, Request::METHOD_POST]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ORMException
|
||||
* @throws OptimisticLockException
|
||||
* @throws TransactionRequiredException
|
||||
*
|
||||
* @return LineItem
|
||||
*/
|
||||
public function createLineItem(array $data)
|
||||
{
|
||||
$caterories = Category::load(null, null, $this->course->getCode());
|
||||
/** @var Category $gradebookCategory */
|
||||
$gradebookCategory = $caterories[0];
|
||||
|
||||
$em = Database::getManager();
|
||||
|
||||
$userId = 1;
|
||||
|
||||
$eval = new Evaluation();
|
||||
$eval->set_user_id($userId);
|
||||
$eval->set_category_id($gradebookCategory->get_id());
|
||||
$eval->set_course_code($this->tool->getCourse()->getCode());
|
||||
$eval->set_name($data['label']);
|
||||
$eval->set_description(null);
|
||||
$eval->set_weight(
|
||||
$gradebookCategory->getRemainingWeight()
|
||||
);
|
||||
$eval->set_max($data['scoreMaximum']);
|
||||
$eval->set_visible(1);
|
||||
$eval->add();
|
||||
|
||||
$evaluation = $em->find('ChamiloCoreBundle:GradebookEvaluation', $eval->get_id());
|
||||
|
||||
$lineItem = new LineItem();
|
||||
$lineItem
|
||||
->setTool($this->tool)
|
||||
->setEvaluation($evaluation)
|
||||
->setResourceId(!empty($data['resourceId']) ? $data['resourceId'] : null)
|
||||
->setTag(!empty($data['tag']) ? $data['tag'] : null);
|
||||
|
||||
if (!empty($data['startDateTime'])) {
|
||||
$startDate = new DateTime($data['startDateTime']);
|
||||
$lineItem->setStartDate($startDate);
|
||||
}
|
||||
|
||||
if (!empty($data['endDateTime'])) {
|
||||
$endDate = new DateTime($data['endDateTime']);
|
||||
$lineItem->setEndDate($endDate);
|
||||
}
|
||||
|
||||
$em->persist($lineItem);
|
||||
$em->flush();
|
||||
|
||||
return $lineItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ORMException
|
||||
* @throws OptimisticLockException
|
||||
* @throws TransactionRequiredException
|
||||
*/
|
||||
private function processPost()
|
||||
{
|
||||
$data = json_decode($this->request->getContent(), true);
|
||||
|
||||
if (empty($data) || empty($data['label']) || empty($data['scoreMaximum'])) {
|
||||
throw new BadRequestHttpException('Missing data to create line item.');
|
||||
}
|
||||
|
||||
$lineItem = $this->createLineItem($data);
|
||||
|
||||
$data['id'] = LtiAssignmentGradesService::getLineItemUrl(
|
||||
$this->course->getId(),
|
||||
$lineItem->getId(),
|
||||
$this->tool->getId()
|
||||
);
|
||||
|
||||
$this->response->headers->set('Content-Type', LtiAssignmentGradesService::TYPE_LINE_ITEM);
|
||||
$this->response->setEncodingOptions(JSON_UNESCAPED_SLASHES);
|
||||
$this->response->setStatusCode(Response::HTTP_CREATED);
|
||||
$this->response->setData($data);
|
||||
}
|
||||
|
||||
private function processGet()
|
||||
{
|
||||
$resourceLinkId = $this->request->query->get('resource_link_id');
|
||||
$resourceId = $this->request->query->get('resource_id');
|
||||
$tag = $this->request->query->get('tag');
|
||||
$limit = $this->request->query->get('limit');
|
||||
$page = $this->request->query->get('page');
|
||||
|
||||
$lineItems = $this->tool->getLineItems($resourceLinkId, $resourceId, $tag, $limit, $page);
|
||||
|
||||
$this->setLinkHeaderToGet($lineItems, $resourceLinkId, $resourceId, $tag, $limit, $page);
|
||||
|
||||
$data = $this->getGetData($lineItems);
|
||||
|
||||
$this->response->headers->set('Content-Type', LtiAssignmentGradesService::TYPE_LINE_ITEM);
|
||||
$this->response->setData($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $resourceLinkId
|
||||
* @param int $resourceId
|
||||
* @param int $tag
|
||||
* @param int $limit
|
||||
* @param int $page
|
||||
*/
|
||||
private function setLinkHeaderToGet(ArrayCollection $lineItems, $resourceLinkId, $resourceId, $tag, $limit, $page)
|
||||
{
|
||||
if (!$limit) {
|
||||
return;
|
||||
}
|
||||
|
||||
$links = [];
|
||||
|
||||
$links['first'] = 0;
|
||||
$links['last'] = ceil($lineItems->count() / $limit);
|
||||
$links['canonical'] = $page;
|
||||
|
||||
if ($page > 1) {
|
||||
$links['prev'] = $page - 1;
|
||||
}
|
||||
|
||||
if ($page + 1 <= $lineItems->count() / $limit) {
|
||||
$links['next'] = $page + 1;
|
||||
}
|
||||
|
||||
foreach ($links as $rel => $linkPage) {
|
||||
$url = LtiAssignmentGradesService::getLineItemsUrl(
|
||||
$this->course->getId(),
|
||||
$this->tool->getId(),
|
||||
[
|
||||
'resource_link_id' => $resourceLinkId,
|
||||
'resource_id' => $resourceId,
|
||||
'tag' => $tag,
|
||||
'limit' => $limit,
|
||||
'page' => $linkPage,
|
||||
]
|
||||
);
|
||||
|
||||
$links[$rel] = '<'.$url.'>; rel="'.$rel.'"';
|
||||
}
|
||||
|
||||
$this->response->headers->set(
|
||||
'Link',
|
||||
implode(', ', $links)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
private function getGetData(ArrayCollection $lineItems)
|
||||
{
|
||||
$data = [];
|
||||
|
||||
/** @var LineItem $lineItem */
|
||||
foreach ($lineItems as $lineItem) {
|
||||
$item = $lineItem->toArray();
|
||||
$item['id'] = LtiAssignmentGradesService::getLineItemUrl(
|
||||
$this->course->getId(),
|
||||
$lineItem->getId(),
|
||||
$this->tool->getId()
|
||||
);
|
||||
|
||||
$data[] = $item;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
235
plugin/ims_lti/src/Service/Resource/LtiResultsResource.php
Normal file
235
plugin/ims_lti/src/Service/Resource/LtiResultsResource.php
Normal file
@@ -0,0 +1,235 @@
|
||||
<?php
|
||||
/* For licensing terms, see /license.txt */
|
||||
|
||||
use Chamilo\CoreBundle\Entity\GradebookResult;
|
||||
use Chamilo\PluginBundle\Entity\ImsLti\LineItem;
|
||||
use Doctrine\ORM\OptimisticLockException;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Doctrine\ORM\TransactionRequiredException;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
|
||||
|
||||
/**
|
||||
* Class LtiResultsResource.
|
||||
*/
|
||||
class LtiResultsResource extends LtiAdvantageServiceResource
|
||||
{
|
||||
const URL_TEMPLATE = '/context_id/lineitems/line_item_id/results';
|
||||
|
||||
/**
|
||||
* @var LineItem|null
|
||||
*/
|
||||
private $lineItem;
|
||||
|
||||
/**
|
||||
* LtiResultsResource constructor.
|
||||
*
|
||||
* @param int $toolId
|
||||
* @param int $courseId
|
||||
* @param int $lineItemId
|
||||
*
|
||||
* @throws ORMException
|
||||
* @throws OptimisticLockException
|
||||
* @throws TransactionRequiredException
|
||||
*/
|
||||
public function __construct($toolId, $courseId, $lineItemId)
|
||||
{
|
||||
parent::__construct($toolId, $courseId);
|
||||
|
||||
$this->lineItem = Database::getManager()->find('ChamiloPluginBundle:ImsLti\LineItem', (int) $lineItemId);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function validate()
|
||||
{
|
||||
if (!$this->course) {
|
||||
throw new BadRequestHttpException('Course not found.');
|
||||
}
|
||||
|
||||
if (!$this->tool) {
|
||||
throw new BadRequestHttpException('Tool not found.');
|
||||
}
|
||||
|
||||
if ($this->tool->getCourse()->getId() !== $this->course->getId()) {
|
||||
throw new AccessDeniedHttpException('Tool not found in course.');
|
||||
}
|
||||
|
||||
if ($this->request->server->get('HTTP_ACCEPT') !== LtiAssignmentGradesService::TYPE_RESULT_CONTAINER) {
|
||||
throw new UnsupportedMediaTypeHttpException('Unsupported media type.');
|
||||
}
|
||||
|
||||
$parentTool = $this->tool->getParent();
|
||||
|
||||
if ($parentTool) {
|
||||
$advServices = $parentTool->getAdvantageServices();
|
||||
|
||||
if (LtiAssignmentGradesService::AGS_NONE === $advServices['ags']) {
|
||||
throw new AccessDeniedHttpException('Assigment and grade service is not enabled for this tool.');
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->lineItem) {
|
||||
throw new NotFoundHttpException('Line item not found');
|
||||
}
|
||||
|
||||
if ($this->lineItem->getTool()->getId() !== $this->tool->getId()) {
|
||||
throw new AccessDeniedHttpException('Line item not found for the tool.');
|
||||
}
|
||||
}
|
||||
|
||||
public function process()
|
||||
{
|
||||
switch ($this->request->getMethod()) {
|
||||
case Request::METHOD_GET:
|
||||
$this->validateToken(
|
||||
[LtiAssignmentGradesService::SCOPE_RESULT_READ]
|
||||
);
|
||||
$this->processGet();
|
||||
break;
|
||||
default:
|
||||
throw new MethodNotAllowedHttpException([Request::METHOD_GET]);
|
||||
}
|
||||
}
|
||||
|
||||
private function processGet()
|
||||
{
|
||||
$limit = $this->request->query->get('limit');
|
||||
$page = $this->request->query->get('page');
|
||||
$userId = $this->request->query->get('user_id');
|
||||
|
||||
$results = $this->getResults($limit, $userId, $page);
|
||||
|
||||
$data = $this->getGetData($results);
|
||||
|
||||
$this->setLinkHeaderToGet($userId, $limit, $page);
|
||||
|
||||
$this->response->headers->set('Content-Type', LtiAssignmentGradesService::TYPE_RESULT_CONTAINER);
|
||||
$this->response->setData($data);
|
||||
}
|
||||
|
||||
private function getResults($limit, $userId, $page = 0)
|
||||
{
|
||||
$em = Database::getManager();
|
||||
|
||||
$limit = (int) $limit;
|
||||
$page = (int) $page;
|
||||
|
||||
$dql = 'SELECT r FROM ChamiloCoreBundle:GradebookResult r WHERE r.evaluationId = :id';
|
||||
$parameters = ['id' => $this->lineItem->getEvaluation()->getId()];
|
||||
|
||||
if ($userId) {
|
||||
$dql .= ' AND r.userId = :user';
|
||||
$parameters['user'] = (int) $userId;
|
||||
}
|
||||
|
||||
$query = $em->createQuery($dql);
|
||||
|
||||
if ($limit > 0) {
|
||||
$query->setMaxResults($limit);
|
||||
|
||||
if ($page > 0) {
|
||||
$query->setFirstResult($page * $limit);
|
||||
}
|
||||
}
|
||||
|
||||
return $query
|
||||
->setParameters($parameters)
|
||||
->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|GradebookResult[] $results
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getGetData(array $results)
|
||||
{
|
||||
$data = [];
|
||||
|
||||
foreach ($results as $result) {
|
||||
$lineItemEndPoint = LtiAssignmentGradesService::getLineItemUrl(
|
||||
$this->course->getId(),
|
||||
$this->lineItem->getId(),
|
||||
$this->tool->getId()
|
||||
);
|
||||
|
||||
$data[] = [
|
||||
'id' => "$lineItemEndPoint/results/{$result->getId()}",
|
||||
'scoreOf' => $lineItemEndPoint,
|
||||
'userId' => (string) $result->getUserId(),
|
||||
'resultScore' => $result->getScore(),
|
||||
'resultMaximum' => $this->lineItem->getEvaluation()->getMax(),
|
||||
'comment' => null,
|
||||
];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $userId
|
||||
* @param int $limit
|
||||
* @param int $page
|
||||
*
|
||||
* @throws \Doctrine\ORM\Query\QueryException
|
||||
*/
|
||||
private function setLinkHeaderToGet($userId, $limit, $page = 0)
|
||||
{
|
||||
$limit = (int) $limit;
|
||||
$page = (int) $page;
|
||||
|
||||
if (!$limit) {
|
||||
return;
|
||||
}
|
||||
|
||||
$em = Database::getManager();
|
||||
|
||||
$dql = 'SELECT COUNT(r) FROM ChamiloCoreBundle:GradebookResult r WHERE r.evaluationId = :id';
|
||||
$parameters = ['id' => $this->lineItem->getEvaluation()->getId()];
|
||||
|
||||
if ($userId) {
|
||||
$dql .= ' AND r.userId = :user';
|
||||
$parameters['user'] = (int) $userId;
|
||||
}
|
||||
|
||||
$count = $em
|
||||
->createQuery($dql)
|
||||
->setParameters($parameters)
|
||||
->getSingleScalarResult();
|
||||
|
||||
$links = [];
|
||||
$links['first'] = 0;
|
||||
$links['last'] = ceil($count / $limit);
|
||||
$links['canonical'] = $page;
|
||||
|
||||
if ($page > 1) {
|
||||
$links['prev'] = $page - 1;
|
||||
}
|
||||
|
||||
if ($page + 1 < $links['last']) {
|
||||
$links['next'] = $page + 1;
|
||||
}
|
||||
|
||||
foreach ($links as $rel => $linkPage) {
|
||||
$url = LtiAssignmentGradesService::getResultsUrl(
|
||||
$this->course->getId(),
|
||||
$this->lineItem->getId(),
|
||||
$this->tool->getId(),
|
||||
['user_id' => $userId, 'limit' => $limit, 'page' => $linkPage]
|
||||
);
|
||||
|
||||
$links[$rel] = '<'.$url.'>; rel="'.$rel.'"';
|
||||
}
|
||||
|
||||
$this->response->headers->set(
|
||||
'Link',
|
||||
implode(', ', $links)
|
||||
);
|
||||
}
|
||||
}
|
||||
214
plugin/ims_lti/src/Service/Resource/LtiScoresResource.php
Normal file
214
plugin/ims_lti/src/Service/Resource/LtiScoresResource.php
Normal file
@@ -0,0 +1,214 @@
|
||||
<?php
|
||||
/* For licensing terms, see /license.txt */
|
||||
|
||||
use Chamilo\CoreBundle\Entity\GradebookResult;
|
||||
use Chamilo\CoreBundle\Entity\GradebookResultLog;
|
||||
use Chamilo\PluginBundle\Entity\ImsLti\LineItem;
|
||||
use Chamilo\UserBundle\Entity\User;
|
||||
use Doctrine\ORM\OptimisticLockException;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Doctrine\ORM\TransactionRequiredException;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
|
||||
|
||||
/**
|
||||
* Class LtiScoresResource.
|
||||
*/
|
||||
class LtiScoresResource extends LtiAdvantageServiceResource
|
||||
{
|
||||
const URL_TEMPLATE = '/context_id/lineitems/line_item_id/results';
|
||||
|
||||
const ACTIVITY_INITIALIZED = 'Initialized';
|
||||
const ACTIVITY_STARTED = 'Started';
|
||||
const ACTIVITY_IN_PROGRESS = 'InProgress';
|
||||
const ACTIVITY_SUBMITTED = 'Submitted';
|
||||
const ACTIVITY_COMPLETED = 'Completed';
|
||||
|
||||
const GRADING_FULLY_GRADED = 'FullyGraded';
|
||||
const GRADING_PENDING = 'Pending';
|
||||
const GRADING_PENDING_MANUAL = 'PendingManual';
|
||||
const GRADING_FAILED = 'Failed';
|
||||
const GRADING_NOT_READY = 'NotReady';
|
||||
|
||||
/**
|
||||
* @var LineItem|null
|
||||
*/
|
||||
private $lineItem;
|
||||
|
||||
/**
|
||||
* LtiScoresResource constructor.
|
||||
*
|
||||
* @param int $toolId
|
||||
* @param int $courseId
|
||||
* @param int $lineItemId
|
||||
*
|
||||
* @throws ORMException
|
||||
* @throws OptimisticLockException
|
||||
* @throws TransactionRequiredException
|
||||
*/
|
||||
public function __construct($toolId, $courseId, $lineItemId)
|
||||
{
|
||||
parent::__construct($toolId, $courseId);
|
||||
|
||||
$this->lineItem = Database::getManager()->find('ChamiloPluginBundle:ImsLti\LineItem', (int) $lineItemId);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function validate()
|
||||
{
|
||||
if (!$this->course) {
|
||||
throw new NotFoundHttpException('Course not found.');
|
||||
}
|
||||
|
||||
if (!$this->lineItem) {
|
||||
throw new NotFoundHttpException('Line item not found');
|
||||
}
|
||||
|
||||
if ($this->lineItem->getTool()->getId() !== $this->tool->getId()) {
|
||||
throw new AccessDeniedHttpException('Line item not found for the tool.');
|
||||
}
|
||||
|
||||
if (!$this->tool) {
|
||||
throw new BadRequestHttpException('Tool not found.');
|
||||
}
|
||||
|
||||
if ($this->tool->getCourse()->getId() !== $this->course->getId()) {
|
||||
throw new AccessDeniedHttpException('Tool not found in course.');
|
||||
}
|
||||
|
||||
if ($this->request->server->get('HTTP_ACCEPT') !== LtiAssignmentGradesService::TYPE_SCORE) {
|
||||
throw new UnsupportedMediaTypeHttpException('Unsupported media type.');
|
||||
}
|
||||
|
||||
$parentTool = $this->tool->getParent();
|
||||
|
||||
if ($parentTool) {
|
||||
$advServices = $parentTool->getAdvantageServices();
|
||||
|
||||
if (LtiAssignmentGradesService::AGS_NONE === $advServices['ags']) {
|
||||
throw new AccessDeniedHttpException('Assigment and grade service is not enabled for this tool.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function process()
|
||||
{
|
||||
switch ($this->request->getMethod()) {
|
||||
case Request::METHOD_POST:
|
||||
$this->validateToken(
|
||||
[LtiAssignmentGradesService::SCOPE_SCORE_WRITE]
|
||||
);
|
||||
$this->processPost();
|
||||
break;
|
||||
default:
|
||||
throw new MethodNotAllowedHttpException([Request::METHOD_POST]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
private function processPost()
|
||||
{
|
||||
$data = json_decode($this->request->getContent(), true);
|
||||
|
||||
if (empty($data) ||
|
||||
!isset($data['userId']) ||
|
||||
!isset($data['gradingProgress']) ||
|
||||
!isset($data['activityProgress']) ||
|
||||
!isset($data['timestamp']) ||
|
||||
(isset($data['timestamp']) && !ImsLti::validateFormatDateIso8601($data['timestamp'])) ||
|
||||
(isset($data['scoreGiven']) && !is_numeric($data['scoreGiven'])) ||
|
||||
(isset($data['scoreGiven']) && !isset($data['scoreMaximum'])) ||
|
||||
(isset($data['scoreMaximum']) && !is_numeric($data['scoreMaximum']))
|
||||
) {
|
||||
throw new BadRequestHttpException('Missing data to create score.');
|
||||
}
|
||||
|
||||
$student = api_get_user_entity($data['userId']);
|
||||
|
||||
if (!$student) {
|
||||
throw new BadRequestHttpException("User (id: {$data['userId']}) not found.");
|
||||
}
|
||||
|
||||
$data['scoreMaximum'] = isset($data['scoreMaximum']) ? $data['scoreMaximum'] : 1;
|
||||
|
||||
$evaluation = $this->lineItem->getEvaluation();
|
||||
|
||||
$result = Database::getManager()
|
||||
->getRepository('ChamiloCoreBundle:GradebookResult')
|
||||
->findOneBy(
|
||||
[
|
||||
'userId' => $data['userId'],
|
||||
'evaluationId' => $evaluation->getId(),
|
||||
]
|
||||
);
|
||||
|
||||
if ($result && $result->getCreatedAt() >= new DateTime($data['timestamp'])) {
|
||||
throw new ConflictHttpException('The timestamp on record is later than the incoming score.');
|
||||
}
|
||||
|
||||
if (isset($data['scoreGiven'])) {
|
||||
if (self::GRADING_FULLY_GRADED !== $data['gradingProgress']) {
|
||||
$data['scoreGiven'] = null;
|
||||
} else {
|
||||
$data['scoreGiven'] = (float) $data['scoreGiven'];
|
||||
|
||||
if ($data['scoreMaximum'] > 0 && $data['scoreMaximum'] != $evaluation->getMax()) {
|
||||
$data['scoreGiven'] = $data['scoreGiven'] * $evaluation->getMax() / $data['scoreMaximum'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$result) {
|
||||
$this->response->setStatusCode(Response::HTTP_CREATED);
|
||||
}
|
||||
|
||||
$this->saveScore($data, $student, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param GradebookResult $result
|
||||
*
|
||||
* @throws OptimisticLockException
|
||||
*/
|
||||
private function saveScore(array $data, User $student, GradebookResult $result = null)
|
||||
{
|
||||
$em = Database::getManager();
|
||||
|
||||
$evaluation = $this->lineItem->getEvaluation();
|
||||
|
||||
if ($result) {
|
||||
$resultLog = new GradebookResultLog();
|
||||
$resultLog
|
||||
->setCreatedAt(api_get_utc_datetime(null, false, true))
|
||||
->setUserId($student->getId())
|
||||
->setEvaluationId($evaluation->getId())
|
||||
->setIdResult($result->getId())
|
||||
->setScore($result->getScore());
|
||||
|
||||
$em->persist($resultLog);
|
||||
} else {
|
||||
$result = new GradebookResult();
|
||||
$result
|
||||
->setUserId($student->getId())
|
||||
->setEvaluationId($evaluation->getId());
|
||||
}
|
||||
|
||||
$result
|
||||
->setCreatedAt(new DateTime($data['timestamp']))
|
||||
->setScore($data['scoreGiven']);
|
||||
|
||||
$em->persist($result);
|
||||
|
||||
$em->flush();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user