Skip to content

Commit 8ed0f2c

Browse files
authored
Merge pull request #9 from Izumi-kun/lti1p3
LTI 1.3 Support
2 parents a9ae534 + 0dbdc7b commit 8ed0f2c

20 files changed

+837
-481
lines changed

LICENSE.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Copyright 2019 Viktor Khokhryakov
1+
Copyright 2024 Viktor Khokhryakov
22

33
Redistribution and use in source and binary forms, with or without
44
modification, are permitted provided that the following conditions

README.md

+35-28
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
Yii2 LTI Tool Provider
1+
Yii2 LTI Tool
22
======================
33

4-
LTI Tool Provider module for Yii2.
4+
LTI Tool module for Yii2.
55

66
[![Latest Stable Version](https://poser.pugx.org/izumi-kun/yii2-lti-tool-provider/v/stable)](https://packagist.org/packages/izumi-kun/yii2-lti-tool-provider)
77
[![Total Downloads](https://poser.pugx.org/izumi-kun/yii2-lti-tool-provider/downloads)](https://packagist.org/packages/izumi-kun/yii2-lti-tool-provider)
@@ -23,27 +23,34 @@ Add namespaced migrations: `izumi\yii2lti\migrations`. Apply new migrations.
2323

2424
### Application config
2525

26-
Add module to web config and configure. The module has three main events for handling messages from Tool Consumers:
26+
Add the module to the web config and configure it. The module supports the following events for handling messages from Platforms:
2727

28-
- `launch` for `basic-lti-launch-request` message type
29-
- `contentItem` for `ContentItemSelectionRequest` message type
30-
- `register` for `ToolProxyRegistrationRequest` message type
28+
- `launch`
29+
- `configure`
30+
- `dashboard`
31+
- `contentItem`
32+
- `contentItemUpdate`
33+
- `submissionReview`
3134

32-
Make sure to configure access to `lti/consumer` controller actions.
33-
All messages from Tool Consumers handles by `lti/connect` controller and there is no access restrictions.
35+
Make sure to configure access to the `lti/platform` controller actions.
36+
All messages from Platforms are handled by the `lti/tool` controller, and there are no access restrictions.
3437

3538
```php
3639
$config = [
3740
'modules' => [
3841
'lti' => [
39-
'class' => '\izumi\yii2lti\Module',
40-
'on launch' => ['\app\controllers\SiteController', 'ltiLaunch'],
41-
'on error' => ['\app\controllers\SiteController', 'ltiError'],
42+
'class' => \izumi\yii2lti\Module::class,
43+
'tool' => [
44+
'debugMode' => YII_DEBUG,
45+
'rsaKey' => 'A PEM formatted private key (for LTI 1.3 support)',
46+
],
47+
'on launch' => [SiteController::class, 'ltiLaunch'],
48+
'on error' => [SiteController::class, 'ltiError'],
4249
'as access' => [
43-
'class' => '\yii\filters\AccessControl',
50+
'class' => \yii\filters\AccessControl::class,
4451
'rules' => [
45-
['allow' => true, 'controllers' => ['lti/connect']],
46-
['allow' => true, 'controllers' => ['lti/consumer'], 'roles' => ['admin']],
52+
['allow' => true, 'controllers' => ['lti/tool']],
53+
['allow' => true, 'controllers' => ['lti/platform'], 'roles' => ['admin']],
4754
],
4855
],
4956
],
@@ -53,12 +60,12 @@ $config = [
5360

5461
### Event handlers
5562

56-
Create event handlers to respect module config.
63+
Create event handlers according to the module configuration.
5764

5865
```php
5966
namespace app\controllers;
6067

61-
use izumi\yii2lti\ToolProviderEvent;
68+
use izumi\yii2lti\ToolEvent;
6269
use Yii;
6370
use yii\web\BadRequestHttpException;
6471
use yii\web\Controller;
@@ -67,9 +74,9 @@ class SiteController extends Controller
6774
{
6875
/**
6976
* basic-lti-launch-request handler
70-
* @param ToolProviderEvent $event
77+
* @param ToolEvent $event
7178
*/
72-
public static function ltiLaunch(ToolProviderEvent $event)
79+
public static function ltiLaunch(ToolEvent $event)
7380
{
7481
$tool = $event->sender;
7582

@@ -78,25 +85,25 @@ class SiteController extends Controller
7885
$isAdmin = $tool->user->isStaff() || $tool->user->isAdmin();
7986

8087
Yii::$app->session->set('isAdmin', $isAdmin);
81-
Yii::$app->session->set('isLtiSession', true);
8288
Yii::$app->session->set('userPk', $userPk);
8389
Yii::$app->controller->redirect(['/site/index']);
8490

8591
$tool->ok = true;
92+
$event->handled = true;
8693
}
8794

8895
/**
8996
* LTI error handler
90-
* @param ToolProviderEvent $event
97+
* @param ToolEvent $event
9198
* @throws BadRequestHttpException
9299
*/
93-
public static function ltiError(ToolProviderEvent $event)
100+
public static function ltiError(ToolEvent $event)
94101
{
95102
$tool = $event->sender;
96103
$msg = $tool->message;
97104
if (!empty($tool->reason)) {
98105
Yii::error($tool->reason);
99-
if ($tool->isDebugMode()) {
106+
if ($tool->debugMode) {
100107
$msg = $tool->reason;
101108
}
102109
}
@@ -108,17 +115,17 @@ class SiteController extends Controller
108115
### Outcome
109116

110117
```php
111-
use IMSGlobal\LTI\ToolProvider;
118+
use ceLTIc\LTI;
112119

113120
/* @var \izumi\yii2lti\Module $module */
114121
$module = Yii::$app->getModule('lti');
115122

116-
$user = ToolProvider\User::fromRecordId(Yii::$app->session->get('userPk'), $module->toolProvider->dataConnector);
123+
$user = $module->findUserById(Yii::$app->session->get('userPk'));
117124

118125
$result = '0.8';
119-
$outcome = new ToolProvider\Outcome($result);
126+
$outcome = new LTI\Outcome($result);
120127

121-
if ($user->getResourceLink()->doOutcomesService(ToolProvider\ResourceLink::EXT_WRITE, $outcome, $user)) {
128+
if ($module->doOutcomesService(LTI\Enum\ServiceAction::Write, $outcome, $user)) {
122129
Yii::$app->session->addFlash('success', 'Result sent successfully');
123130
}
124131
```
@@ -129,5 +136,5 @@ if ($user->getResourceLink()->doOutcomesService(ToolProvider\ResourceLink::EXT_W
129136

130137
### Useful
131138

132-
- [LTI Tool Consumer emulator](https://lti.tools/saltire/tc)
133-
- [IMSGlobal/LTI-Tool-Provider-Library-PHP/wiki](https://github.com/IMSGlobal/LTI-Tool-Provider-Library-PHP/wiki)
139+
- [LTI Platform emulator](https://saltire.lti.app/platform)
140+
- [celtic-project/LTI-PHP/wiki](https://github.com/celtic-project/LTI-PHP/wiki)

composer.json

+15-4
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
{
22
"name": "izumi-kun/yii2-lti-tool-provider",
3-
"description": "LTI Tool Provider module for Yii2",
3+
"description": "LTI Tool module for Yii2",
44
"type": "yii2-extension",
55
"require": {
6-
"php": ">=5.4.0",
7-
"yiisoft/yii2": "~2.0.14",
6+
"php": ">=8.1",
7+
"yiisoft/yii2": "~2.0.51",
88
"yiisoft/yii2-httpclient": "~2.0.0",
9-
"izumi-kun/lti": "~1.2.0"
9+
"celtic/lti": "^v5.1.0"
1010
},
1111
"scripts": {
1212
"messages": "yii message/extract src/messages/config.php"
@@ -18,9 +18,20 @@
1818
"email": "viktor.khokhryakov@gmail.com"
1919
}
2020
],
21+
"repositories": [
22+
{
23+
"type": "composer",
24+
"url": "https://asset-packagist.org"
25+
}
26+
],
2127
"autoload": {
2228
"psr-4": {
2329
"izumi\\yii2lti\\": "src"
2430
}
31+
},
32+
"config": {
33+
"allow-plugins": {
34+
"yiisoft/yii2-composer": true
35+
}
2536
}
2637
}

src/HttpClient.php

+9-13
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
<?php
22
/**
33
* @link https://github.com/Izumi-kun/yii2-lti-tool-provider
4-
* @copyright Copyright (c) 2019 Viktor Khokhryakov
4+
* @copyright Copyright (c) 2024 Viktor Khokhryakov
55
* @license http://opensource.org/licenses/BSD-3-Clause
66
*/
77

88
namespace izumi\yii2lti;
99

10-
use IMSGlobal\LTI\Http\ClientInterface;
11-
use IMSGlobal\LTI\HTTPMessage;
10+
use ceLTIc\LTI\Http\ClientInterface;
11+
use ceLTIc\LTI\Http\HttpMessage;
1212

1313
/**
1414
* Class HttpClient
@@ -21,26 +21,22 @@ class HttpClient implements ClientInterface
2121
/**
2222
* @inheritdoc
2323
*/
24-
public function send(HTTPMessage $message)
24+
public function send(HttpMessage $message): bool
2525
{
2626
$request = Module::getInstance()->httpClient->createRequest()
27-
->setMethod($message->method)
28-
->setUrl($message->url)
27+
->setMethod($message->getMethod())
28+
->setUrl($message->getUrl())
2929
->setContent($message->request);
3030

3131
if (!empty($message->requestHeaders)) {
32-
if (is_string($message->requestHeaders)) {
33-
$request->setHeaders(explode("\n", $message->requestHeaders));
34-
} elseif (is_array($message->requestHeaders)) {
35-
$request->setHeaders(array_values($message->requestHeaders));
36-
}
37-
$message->requestHeaders = implode("\n", $request->composeHeaderLines());
32+
$request->setHeaders(array_values($message->requestHeaders));
33+
$message->requestHeaders = $request->composeHeaderLines();
3834
}
3935

4036
$resp = $request->send();
4137
$message->status = $resp->statusCode;
4238
$message->response = $resp->getContent();
43-
$message->responseHeaders = implode("\n", $resp->composeHeaderLines());
39+
$message->responseHeaders = $resp->composeHeaderLines();
4440

4541
return $resp->isOk;
4642
}

src/Module.php

+71-17
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
11
<?php
22
/**
33
* @link https://github.com/Izumi-kun/yii2-lti-tool-provider
4-
* @copyright Copyright (c) 2019 Viktor Khokhryakov
4+
* @copyright Copyright (c) 2024 Viktor Khokhryakov
55
* @license http://opensource.org/licenses/BSD-3-Clause
66
*/
77

88
namespace izumi\yii2lti;
99

10-
use IMSGlobal\LTI\HTTPMessage;
11-
use IMSGlobal\LTI\ToolProvider\DataConnector\DataConnector_pdo;
10+
use ceLTIc\LTI\DataConnector\DataConnector;
11+
use ceLTIc\LTI\Enum\LtiVersion;
12+
use ceLTIc\LTI\Enum\ServiceAction;
13+
use ceLTIc\LTI\Http\HttpMessage;
14+
use ceLTIc\LTI\Outcome;
15+
use ceLTIc\LTI\Tool as BaseTool;
16+
use ceLTIc\LTI\UserResult;
1217
use Yii;
1318
use yii\db\Connection;
1419
use yii\di\Instance;
20+
use yii\helpers\Url;
1521
use yii\httpclient\Client;
22+
use yii\i18n\PhpMessageSource;
1623

1724
/**
1825
* Module.
@@ -22,22 +29,29 @@
2229
class Module extends \yii\base\Module
2330
{
2431
const EVENT_LAUNCH = 'launch';
25-
const EVENT_REGISTER = 'register';
32+
const EVENT_CONFIGURE = 'configure';
33+
const EVENT_DASHBOARD = 'dashboard';
2634
const EVENT_CONTENT_ITEM = 'contentItem';
35+
const EVENT_CONTENT_ITEM_UPDATE = 'contentItemUpdate';
36+
const EVENT_SUBMISSION_REVIEW = 'submissionReview';
2737
const EVENT_ERROR = 'error';
2838

2939
/**
30-
* @var ToolProvider|array
40+
* @var Tool|array
3141
*/
32-
public $toolProvider = [];
42+
public array|Tool $tool = [];
3343
/**
3444
* @var Client|array|string
3545
*/
36-
public $httpClient = [];
46+
public string|Client|array $httpClient = [];
3747
/**
3848
* @var Connection|array|string the DB connection object or the application component ID of the DB connection.
3949
*/
40-
public $db = 'db';
50+
public Connection|string|array $db = 'db';
51+
/**
52+
* @var string
53+
*/
54+
public string $lti1p3SignatureMethod = 'RS256';
4155

4256
/**
4357
* @inheritdoc
@@ -49,22 +63,62 @@ public function init()
4963
$i18n = Yii::$app->i18n;
5064
if (!isset($i18n->translations['lti']) && !isset($i18n->translations['lti*'])) {
5165
$i18n->translations['lti'] = [
52-
'class' => '\yii\i18n\PhpMessageSource',
66+
'class' => PhpMessageSource::class,
5367
'basePath' => __DIR__ . '/messages',
5468
'sourceLanguage' => 'en',
5569
];
5670
}
5771

58-
$this->db = Instance::ensure($this->db, Connection::className());
72+
$this->db = Instance::ensure($this->db, Connection::class);
73+
74+
$toolConfig = is_array($this->tool) ? $this->tool : [];
75+
$toolConfig['dataConnector'] = DataConnector::getDataConnector($this->db->getMasterPdo(), $this->db->tablePrefix);
76+
if (!isset($toolConfig['baseUrl'])) {
77+
$toolConfig['baseUrl'] = Yii::$app->getRequest()->getHostInfo();
78+
}
79+
if (!array_key_exists('jku', $toolConfig)) {
80+
$toolConfig['jku'] = Url::to($this->getUniqueId() . '/tool/jwks', true);
81+
}
82+
$this->tool = Instance::ensure($toolConfig, Tool::class);
5983

60-
$tpConfig = is_array($this->toolProvider) ? $this->toolProvider : [];
61-
$tpConfig['dataConnector'] = new DataConnector_pdo($this->db->getMasterPdo(), $this->db->tablePrefix);
62-
if (!isset($tpConfig['baseUrl'])) {
63-
$tpConfig['baseUrl'] = Yii::$app->getRequest()->getHostInfo();
84+
if ($this->tool->rsaKey && openssl_pkey_get_private($this->tool->rsaKey) === false) {
85+
$this->tool->rsaKey = null;
86+
Yii::warning('rsaKey is not valid private key', __METHOD__);
6487
}
65-
$this->toolProvider = Instance::ensure($tpConfig, '\izumi\yii2lti\ToolProvider');
6688

67-
$this->httpClient = Instance::ensure($this->httpClient, Client::className());
68-
HTTPMessage::setHttpClient(new HttpClient());
89+
$this->httpClient = Instance::ensure($this->httpClient, Client::class);
90+
HttpMessage::setHttpClient(new HttpClient());
91+
BaseTool::$defaultTool = $this->tool;
92+
}
93+
94+
/**
95+
* Load the user from the database.
96+
* @param int $id
97+
* @return UserResult|null
98+
*/
99+
public function findUserById(int $id): ?UserResult
100+
{
101+
$user = UserResult::fromRecordId($id, $this->tool->dataConnector);
102+
if (!$user->getResourceLink()) {
103+
return null;
104+
}
105+
return $user;
106+
}
107+
108+
/**
109+
* Perform an Outcomes service request.
110+
* @param ServiceAction $action
111+
* @param Outcome $ltiOutcome
112+
* @param UserResult $user
113+
* @return bool
114+
*/
115+
public function doOutcomesService(ServiceAction $action, Outcome $ltiOutcome, UserResult $user): bool
116+
{
117+
$resourceLink = $user->getResourceLink();
118+
if (!$resourceLink) {
119+
return false;
120+
}
121+
$this->tool->signatureMethod = $resourceLink->getPlatform()->ltiVersion === LtiVersion::V1P3 ? $this->lti1p3SignatureMethod : 'HMAC-SHA1';
122+
return $resourceLink->doOutcomesService($action, $ltiOutcome, $user);
69123
}
70124
}

0 commit comments

Comments
 (0)