Actualización

This commit is contained in:
Xes
2025-04-10 12:36:07 +02:00
parent 1da7c3f3b9
commit 4aff98e77b
3147 changed files with 320647 additions and 0 deletions

View 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);
}

View 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;
}
}

View 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
}
}

View 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(),
]
);
}
}

View 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;
}
}

View 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);
}
}

View 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');
}
}

View 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;
}
}

View 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);
}
}

View 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
)
);
}
}

View 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');
}
}

View 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;
}
}

View 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;
}
}

View 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);
}

View 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;
}
}

View 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;
}
}

View 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'
);
}
}

View 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)
{
}
}

View 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);
}
}

View 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;
}
}

View 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);
}

View 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";
}
}

View File

@@ -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";
}
}

View File

@@ -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.');
}
}
}

View File

@@ -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"'
);
}
}
}

View 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();
}
}

View 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;
}
}

View 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)
);
}
}

View 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();
}
}