Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ $resource = LTI\LTI_Deep_Link_Resource::new()

Everything is set to return the resource to the platform. There are two methods of doing this.

The following method will output the html for an aut-posting form for you.
The following method will output the html for an auto-posting form for you.
```php
$dl->output_response_form([$resource]);
```
Expand All @@ -209,6 +209,12 @@ Alternatively you can just request the signed JWT that will need posting back to
$dl->get_response_jwt([$resource]);
```

If you've created a JWKS endpoint with `LTI\JWKS_Endpoint::new()`, the kid used in the endpoint can be provided as an additional parameter.
```php
$dl->get_response_jwt([$resource], 'a_unique_KID');

```

## Calling Services
### Names and Roles Service

Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "imsglobal/lti-1p3-tool",
"type": "library",
"require": {
"fproject/php-jwt": "^4.0",
"firebase/php-jwt": "^6",
"phpseclib/phpseclib": "^2.0"
},
"autoload": {
Expand Down
19 changes: 19 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
parameters:
parallel:
maximumNumberOfProcesses: 1
tmpDir: /tmp/phpstan-%env.USER%
ignoreErrors:
- '%Access to constant PUBLIC_FORMAT_PKCS8 on an unknown class phpseclib\\Crypt\\RSA\.%'
- '%Access to property \$modulus on an unknown class phpseclib\\Crypt\\RSA%'
- '%Access to property \$publicExponent on an unknown class phpseclib\\Crypt\\RSA.%'
- '%Access to static property \$leeway on an unknown class Firebase\\JWT\\JWT\.%'
- '%Call to method loadKey\(\) on an unknown class phpseclib\\Crypt\\RSA\.%'
- '%Call to method setHash\(\) on an unknown class phpseclib\\Crypt\\RSA\.%'
- '%Call to method setPublicKey\(\) on an unknown class phpseclib\\Crypt\\RSA\.%'
- '%Call to static method decode\(\) on an unknown class Firebase\\JWT\\JWT\.%'
- '%Call to static method encode\(\) on an unknown class Firebase\\JWT\\JWT\.%'
- '%Call to static method parseKey\(\) on an unknown class Firebase\\JWT\\JWK\.%'
- '%Call to static method urlsafeB64Decode\(\) on an unknown class Firebase\\JWT\\JWT\.%'
- '%Call to static method urlsafeB64Encode\(\) on an unknown class Firebase\\JWT\\JWT\.%'
- '%Instantiated class Firebase\\JWT\\Key not found\.%'
- '%Instantiated class phpseclib\\Crypt\\RSA not found\.%'
14 changes: 14 additions & 0 deletions phpunit.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" bootstrap="vendor/autoload.php" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd">
<coverage>
<include>
<directory suffix=".php">src/lti</directory>
<directory suffix=".php">src/lti/message_validators</directory>
</include>
</coverage>
<testsuites>
<testsuite name="LTI Tests">
<directory>tests</directory>
</testsuite>
</testsuites>
</phpunit>
35 changes: 24 additions & 11 deletions src/lti/Cache.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,44 +3,57 @@

class Cache {

private $cache;
/** @var array<mixed> $cache */
private array $cache;

public function get_launch_data($key) {
public function get_launch_data(string $key): mixed {
$this->load_cache();
return $this->cache[$key];
}

public function cache_launch_data($key, $jwt_body) {
/**
* @param array<mixed> $jwt_body
*/
public function cache_launch_data(string $key, array $jwt_body): self {
$this->cache[$key] = $jwt_body;
$this->save_cache();
return $this;
}

public function cache_nonce($nonce) {
$this->cache['nonce'][$nonce] = true;
public function cache_nonce(string $nonce): self {
$this->cache['nonce'][$nonce] = false;
$this->save_cache();
return $this;
}

public function check_nonce($nonce) {
public function check_nonce(string $nonce): bool {
$this->load_cache();
if (!isset($this->cache['nonce'][$nonce])) {
return false;
}
if ($this->cache['nonce'][$nonce]) {
return false;
}
$this->cache['nonce'][$nonce] = true;
$this->save_cache();
return true;
}

private function load_cache() {
private function load_cache(): void {
$cache = file_get_contents(sys_get_temp_dir() . '/lti_cache.txt');
if (empty($cache)) {
if ($cache === false) {
file_put_contents(sys_get_temp_dir() . '/lti_cache.txt', '{}');
$this->cache = [];
return;
}
$data = json_decode($cache, true);
if ($data === false) {
throw new \Exception("Failed to decode cache contents.");
}
$this->cache = json_decode($cache, true);
$this->cache = $data;
}

private function save_cache() {
private function save_cache(): void {
file_put_contents(sys_get_temp_dir() . '/lti_cache.txt', json_encode($this->cache));
}
}
?>
11 changes: 8 additions & 3 deletions src/lti/Cookie.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
namespace IMSGlobal\LTI;

class Cookie {
public function get_cookie($name) {
/**
* @return string|false
*/
public function get_cookie(string $name) {
if (isset($_COOKIE[$name])) {
return $_COOKIE[$name];
}
Expand All @@ -13,7 +16,10 @@ public function get_cookie($name) {
return false;
}

public function set_cookie($name, $value, $exp = 3600, $options = []) {
/**
* @param array<mixed> $options
*/
public function set_cookie(string $name, string $value, int $exp = 3600, array $options = []): self {
$cookie_options = [
'expires' => time() + $exp
];
Expand All @@ -31,4 +37,3 @@ public function set_cookie($name, $value, $exp = 3600, $options = []) {
return $this;
}
}
?>
6 changes: 2 additions & 4 deletions src/lti/Database.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
namespace IMSGlobal\LTI;

interface Database {
public function find_registration_by_issuer($iss);
public function find_deployment($iss, $deployment_id);
public function find_registration_by_issuer(string $iss, ?string $client_id): ?LTI_Registration;
public function find_deployment(string $iss, string $deployment_id): ?LTI_Deployment;
}

?>
41 changes: 30 additions & 11 deletions src/lti/JWKS_Endpoint.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,51 @@

class JWKS_Endpoint {

private $keys;
/** @var array<mixed> $keys */
private array $keys;

/**
* @param array<mixed> $keys
*/
public function __construct(array $keys) {
$this->keys = $keys;
}

public static function new($keys) {
/**
* @param array<mixed> $keys
*/
public static function new(array $keys): self {
return new JWKS_Endpoint($keys);
}

public static function from_issuer(Database $database, $issuer) {
$registration = $database->find_registration_by_issuer($issuer);
return new JWKS_Endpoint([$registration->get_kid() => $registration->get_tool_private_key()]);
public static function from_issuer(
Database $database,
string $issuer,
string $client_id
): self {
$registration = $database->find_registration_by_issuer($issuer, $client_id);
if ($registration === null) {
throw new LTI_Exception("Could not find registration");
}
return new self([$registration->get_kid() => $registration->get_tool_private_key()]);
}

public static function from_registration(LTI_Registration $registration) {
return new JWKS_Endpoint([$registration->get_kid() => $registration->get_tool_private_key()]);
public static function from_registration(
LTI_Registration $registration
): self {
return new self([$registration->get_kid() => $registration->get_tool_private_key()]);
}

public function get_public_jwks() {
/**
* @return array<mixed>
*/
public function get_public_jwks(): array {
$jwks = [];
foreach ($this->keys as $kid => $private_key) {
$key = new RSA();
$key->setHash("sha256");
$key->loadKey($private_key);
$key->setPublicKey(false, RSA::PUBLIC_FORMAT_PKCS8);
$key->setPublicKey("", RSA::PUBLIC_FORMAT_PKCS8);
if ( !$key->publicExponent ) {
continue;
}
Expand All @@ -48,8 +67,8 @@ public function get_public_jwks() {
return ['keys' => $jwks];
}

public function output_jwks() {
public function output_jwks(): void {
echo json_encode($this->get_public_jwks());
}

}
}
30 changes: 19 additions & 11 deletions src/lti/LTI_Assignments_Grades_Service.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,22 @@
namespace IMSGlobal\LTI;

class LTI_Assignments_Grades_Service {
private LTI_Service_Connector $service_connector;
/** @var array<mixed> $service_data */
private array $service_data;

private $service_connector;
private $service_data;

public function __construct(LTI_Service_Connector $service_connector, $service_data) {
/**
* @param array<mixed> $service_data
*/
public function __construct(LTI_Service_Connector $service_connector, array $service_data) {
$this->service_connector = $service_connector;
$this->service_data = $service_data;
}

public function put_grade(LTI_Grade $grade, LTI_Lineitem $lineitem = null) {
/**
* @return array<mixed>
*/
public function put_grade(LTI_Grade $grade, ?LTI_Lineitem $lineitem = null): array {
if (!in_array("https://purl.imsglobal.org/spec/lti-ags/scope/score", $this->service_data['scope'])) {
throw new LTI_Exception('Missing required scope', 1);
}
Expand All @@ -24,7 +30,7 @@ public function put_grade(LTI_Grade $grade, LTI_Lineitem $lineitem = null) {
} else {
$lineitem = LTI_Lineitem::new()
->set_label('default')
->set_score_maximum(100);
->set_score_maximum('100');
$lineitem = $this->find_or_create_lineitem($lineitem);
$score_url = $lineitem->get_id();
}
Expand All @@ -41,7 +47,7 @@ public function put_grade(LTI_Grade $grade, LTI_Lineitem $lineitem = null) {
);
}

public function find_or_create_lineitem(LTI_Lineitem $new_line_item) {
public function find_or_create_lineitem(LTI_Lineitem $new_line_item): LTI_Lineitem {
if (!in_array("https://purl.imsglobal.org/spec/lti-ags/scope/lineitem", $this->service_data['scope'])) {
throw new LTI_Exception('Missing required scope', 1);
}
Expand All @@ -50,7 +56,7 @@ public function find_or_create_lineitem(LTI_Lineitem $new_line_item) {
'GET',
$this->service_data['lineitems'],
null,
null,
'application/json',
'application/vnd.ims.lis.v2.lineitemcontainer+json'
);
foreach ($line_items['body'] as $line_item) {
Expand All @@ -71,7 +77,10 @@ public function find_or_create_lineitem(LTI_Lineitem $new_line_item) {
return new LTI_Lineitem($created_line_item['body']);
}

public function get_grades(LTI_Lineitem $lineitem) {
/**
* @return array<mixed>
*/
public function get_grades(LTI_Lineitem $lineitem): array {
$lineitem = $this->find_or_create_lineitem($lineitem);
// Place '/results' before url params
$pos = strpos($lineitem->get_id(), '?');
Expand All @@ -81,11 +90,10 @@ public function get_grades(LTI_Lineitem $lineitem) {
'GET',
$results_url,
null,
null,
'application/json',
'application/vnd.ims.lis.v2.resultcontainer+json'
);

return $scores['body'];
}
}
?>
Loading