-
Notifications
You must be signed in to change notification settings - Fork 38
Usage
This page describes the usage of version 4 of the library; follow this link for details relating to [version 3](Usage version 3).
Please refer to the class definition documentation for details of the classes defined in this library. The sample Rating application provides a working example of the usage of this library.
A data connector instance is required to initialise the entities defined in this library. This is a mechanism for abstracting the data persistence from the application code and allows support for different databases as well as bespoke implementations based on an existing table structure within an application. It is also possible to set up a dummy application which does not persist any data.
When using the PHP MySQL library, connect to the database in the normal way (using a server name, user name and password) and use the MySQL link identifier which is returned to create the data connector. A table name prefix (e.g. 'app1_') can be optionally included.
use ceLTIc\LTI\DataConnector;
$db = mysql_connect($dbServer, $dbUser, $dbPassword);
mysql_select_db($dbSchema);
$dataConnector = DataConnector\DataConnector::getDataConnector($db, 'app1_');
When using the PHP MySQLi library, connect to the database in the normal way (using a server name, user name and password) and use the MySQLi link identifier which is returned to create the data connector. A table name prefix (e.g. 'app1_') can be optionally included.
use ceLTIc\LTI\DataConnector;
$db = mysqli_connect($dbServer, $dbUser, $dbPassword);
mysqli_select_db($dbSchema);
$dataConnector = DataConnector\DataConnector::getDataConnector($db, 'app1_');
When using the PHP sqlsrv library, connect to the database in the normal way (using a server name, user name and password) and use the sqlsrv link identifier which is returned to create the data connector. A table name prefix (e.g. 'app1_') can be optionally included.
use ceLTIc\LTI\DataConnector;
/*
* Example: $dbServer = '(local)';
*/
$connectionInfo = array('Database' => $dbServer, 'UID' => $dbUser, 'PWD' => $dbPassword);
$db = sqlsrv_connect($dbSchema, $connectionInfo);
$dataConnector = DataConnector\DataConnector::getDataConnector($db, 'app1_', 'sqlsrv');
(This has been tested with SQL Sever 2017.)
When using the PHP oci library, connect to the database in the normal way (using a server name, user name and password) and use the oci link identifier which is returned to create the data connector. A table name prefix (e.g. 'app1_') can be optionally included.
use ceLTIc\LTI\DataConnector;
/*
* Example: $dbSchema = '//localhost:1521/orcl';
*/
$db = oci_connect($dbSchema, $dbUser, $dbPassword);
$dataConnector = DataConnector\DataConnector::getDataConnector($db, 'app1_', 'oci');
(This has been tested with Oracle 12.2.)
When using the PHP pg library, connect to the database in the normal way (using a server name, user name and password) and use the pg link identifier which is returned to create the data connector. A table name prefix (e.g. 'app1_') can be optionally included.
use ceLTIc\LTI\DataConnector;
/*
Example: $dbServer= 'host=localhost port=5432 dbname=test';
*/
$db = pg_connect($dbServer, $dbUser, $dbPassword);
$dataConnector = DataConnector\DataConnector::getDataConnector($db, 'app1_', 'pg');
(This has been tested with PostgreSQL 9.5.3.)
Database connections can be also be made using the PHP PDO interface. Here are some examples.
use ceLTIc\LTI\DataConnector;
$db = new PDO("mysql:host={$dbHost};dbname={$dbSchema}", $dbUser, $dbPassword);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); // optional
$dataConnector = DataConnector\DataConnector::getDataConnector($db);
use ceLTIc\LTI\DataConnector;
$db = new PDO('sqlite::memory:');
$dataConnector = DataConnector\DataConnector::getDataConnector($db);
use ceLTIc\LTI\DataConnector;
$db = new PDO("mssql:host={$dbHost};dbname={$dbSchema}", $dbUser, $dbPassword);
$dataConnector = DataConnector\DataConnector::getDataConnector($db);
(This has been tested with SQL Sever 2017.)
use ceLTIc\LTI\DataConnector;
$db = new PDO("oci:dbname={$dbSchema}", $dbUser, $dbPassword);
$dataConnector = DataConnector\DataConnector::getDataConnector($db);
(This has been tested with Oracle 12.2.)
use ceLTIc\LTI\DataConnector;
$db = new PDO("pgsql:host=localhost;port=5432;dbname=test", $dbUser, $dbPassword);
$dataConnector = DataConnector\DataConnector::getDataConnector($db);
(This has been tested with PostgreSQL 9.5.3.)
Bespoke connectors can be created by writing a sub-class of the DataConnector\DataConnector
class. This allows the data for the different LTI classes to be held in tables of your own design; those classes which are not required by your application can be ignored (just using the default method definition which assumes no data persistence).
When a launch request is received by a tool it will be validated with the shared secret associated with the consumer key. The default data structure uses the lti2_consumer
table to record details of platforms. A record may be initialized in this table for an LTI 1.0/1.1/1.2 platform as follows (records for LTI 2 platforms will be created as part of the registration process):
use ceLTIc\LTI;
$platform = LTI\Platform::fromConsumerKey('testing.edu', $dataConnector);
$platform->name = 'Testing';
$platform->secret = 'ThisIsASecret!';
$platform->enabled = true;
$platform->save();
...
use ceLTIc\LTI;
$platform = LTI\Platform::fromPlatformId('https://platform.edu', 'dlirkvlkvlk', 'epodvvlklrlkrto', $dataConnector);
$platform->name = 'Testing';
$platform->authorizationServerId = null; // defaults to the Access Token URL
$platform->authenticationUrl = 'https://platform.edu/auth';
$platform->accessTokenUrl = 'https://platform.edu/token';
$platform->rsaKey = null;
$platform->jku = 'https://platform.edu/jwks';
$platform->signatureMethod = 'RS256';
$platform->enabled = true;
$platform->save();
The rsaKey
property is used to record a platform's public key; this may be specified in either PEM format or JSON format (as returned from a JWKS endpoint). It is not required if a jku
endpoint is available, although the library does not currently cache public keys retrieved from such an endpoint so there is a performance gain from recording the public key in the platform's record. If a signature verification fails when using the public key specified and a jku endpoint is also defined, then a second verification will be attempted using th public key retrieved from this endpoint. Thus, the verification should not fail when a key is changed; the system will fallback to the retrieving the new one. The failure of the verification with the public key will be reported as an error, so if you have error-level logging enabled, you can check your log files for reports of outdated keys.
A platform may be defined which supports all versions of LTI by combining the above examples to record both sets of details. It is important that, when unused, the Platform ID, Client ID and Deployment ID properties are set to a value of null
(the default) rather than an empty string to ensure that unique database constraints are not violated.
The primary use case for the classes is to validate an incoming launch request from a platform. Once a record has been initialised for the platform (see above), the verification of the authenticity of the LTI launch request is handled automatically by the Tool
class. A sub-class is created and the onLaunch
method overridden to define the code to be run when a valid launch request is received.
use ceLTIc\LTI;
class MyApp extends LTI\Tool {
function onLaunch() {
// Insert code here to handle incoming connections - use the user,
// context and resourceLink properties of the class instance
// to access the current user, context and resource link.
}
}
$tool = new MyApp($dataConnector);
$tool->handleRequest();
The handleRequest
method checks the authenticity of the incoming request. For LTI 1.0/1.1/1.2/2.0 it does this by verifying:
- the OAuth signature (using the shared secret recorded for the platform),
- the timestamp is within a defined limit of the current time, and
- the nonce value has not been previously used.
For requests using LTI 1.3 a request is automatically sent to the platform's Authentication URL with JWT returned being verified using the platform's public key (either as recorded in the database or as retrieved from the JSON Web Key URL) and the current time.
Only if the request passes all the checks is the onLaunch
method called. The process also captures various standard launch parameters to allow access to services.
When a launch is not valid, a message is returned to the platform with a more detailed reason to be logged. If the tool's debug mode is set then the more detailed reason for the failure is also displayed to the user. Debug mode can be set in your code; for example:
use ceLTIc\LTI;
class MyApp extends LTI\Tool {
function __construct($dataConnector)
{
parent::__construct($dataConnector);
$this->debugMode = true;
}
...
}
or, alternatively:
$tool = new MyApp($dataConnector);
$tool->debugMode = true;
$tool->handleRequest();
In adition a platform can trigger debug mode by including a custom parameter of debug=true
in its launch message.
When debug mode is active more detailed messages are also sent to the PHP error log. These debug log messages can also be activated on a per-platform basis by saving a platform with its debug mode set; for example:
use ceLTIc\LTI;
$platform = LTI\Platform::fromConsumerKey('testing.edu', $dataConnector);
$platform->name = 'Testing';
$platform->secret = 'ThisIsASecret!';
$platform->enabled = true;
$platform->debugMode = true;
$platform->save();
This allows debug-level logging to be activated for specific platforms only.
The onLaunch
method may be used to:
- create the user account if it does not already exist (or update it if it does);
- create any workspace required for the resource link if it does not already exist (or update it if it does);
- establish a new session for the user (or otherwise log the user into the tool application);
- keep a record of the return URL for the platform (for example, in a session variable);
- set the URL for the home page of the application so the user may be redirected to it.
Even though a request may be in accordance with the LTI specification, a tool may still choose to reject it because, for example, not all of the required data has been passed. A request may be rejected as follows:
- set the
ok
property tofalse
; - optionally set an error message to return to the user (if the platform supports this facility).
For example:
function onLaunch() {
...
$this->ok = false;
$this->reason = 'Incomplete data in launch message';
}
If your tool also supports the Content-Item (Deep Linking) message or LTI 2 registration messages, then their associated method should also be overridden; for example:
use ceLTIc\LTI;
class MyApp extends LTI\Tool {
function onLaunch() {
// Insert code here to handle incoming launches - use the user, context
// and resourceLink properties to access the current user, context and resource link.
}
function onContentItem() {
// Insert code here to handle incoming content-item requests - use the user and context
// properties to access the current user and context.
}
function onRegister() {
// Insert code here to handle incoming registration requests - use the user
// property to access the current user.
}
function onError() {
// Insert code here to handle errors on incoming connections - do not expect
// the user, context and resourceLink properties to be populated but check the reason
// property for the cause of the error. Return TRUE if the error was fully
// handled by this method.
}
}
$tool = new MyApp($dataConnector);
$tool->handleRequest();
When a connection between a platform and a tool is secured using a consumer key and a shared secret there are some risks to be aware of:
- launch requests will continue to be accepted even if the platform's license has expired;
- if the consumer key is used to submit launch requests from more than one platform, there is a risk of clashing resource link and user IDs being received from each.
The first risk can be avoided by manually removing or disabling the consumer key as soon as the license expires. Alternatively, the dates for any license may be recorded for the platform so that the library automatically makes the appropriate check when a launch request is received.
The second risk can be alleviated by using LTI 1.3 or by setting the protected property of the tool consumer. This will cause launch requests to be only accepted from platforms with the same tool_consumer_guid
parameter. The value of this parameter is recorded from the first launch request received using the associated consumer key. Note that this facility depends upon the platform sending a value for the tool_consumer_guid
parameter and each tool consumer instance having a unique value for this parameter.
The following code illustrates how these options may be set:
use ceLTIc\LTI;
// load the platform record
$platform = LTI\Platform::fromConsumerKey('testing.edu', $dataConnector);
// set an expiry date for 30 days time
$platform->enableUntil = time() + (30 * 24 * 60 * 60);
// protect use of the consumer key to a single platform
$platform->protected = true;
// save the changes
$platform->save();
Note that the default value of the enableFrom
property is null
which means that access is available immediately. A null
value for the enableUntil
property means that access does not automatically expire (this is also the default).
One of the differences in handling a content-item/deep linking message request is that any LTI links your tool passes back to be created will not yet have an associated resource link ID. One solution to this is to create an internal resource link ID for the resource and add this as a custom parameter to the link with a name of content_item_id
. When a launch request is received from a resource link ID which is not recognised and this custom parameter is present, a check is made for a resource link with the value of the parameter. If found, the resource link ID is updated with the resource link ID from the launch request and the custom parameter will be ignored on any subsequent launches. In this way, the resource created via a content-item request will be automatically connected to the resource link created in the platform. For example, here is some sample code based on this workflow implemented in the sample Rating application:
...
$item = new LTI\ContentItem('LtiLink');
$item->setMediaType(LTI\ContentItem::LTI_LINK_MEDIA_TYPE);
$item->setTitle($_SESSION['title']);
$item->setText($_SESSION['text']);
$item->icon = new LTI\ContentItemImage(getAppUrl() . 'images/icon50.png', 50, 50);
$item->custom = array('content_item_id' => $_SESSION['resource_id']);
$formParams['content_items'] = LTI\ContentItem::toJson($item);
if (!is_null($_SESSION['data'])) {
$formParams['data'] = $_SESSION['data'];
}
$dataConnector = LTI\DataConnector\DataConnector::getDataConnector($db, DB_TABLENAME_PREFIX);
$platform = LTI\Platform::fromRecordId($_SESSION['consumer_pk'], $dataConnector);
$formParams = $platform->signParameters($_SESSION['return_url'], 'ContentItemSelection', $_SESSION['lti_version'], $formParams);
$page = LTI\Util::sendForm($_SESSION['return_url'], $formParams);
echo $page;
exit;
...
The $_SESSION['resource_id'] variable contains a GUID generated on launch; this is used as the placeholder until the first launch of this item is performed and the validation of the request will automatically replace this resource link ID with the one passed in the launch parameters.
There is typically a one-to-one relationship between a link created in a platform and a resource in a tool. When the resurces offered by a tool are sharable items, such as content, this might be a many-to-one relationship, where many links in one or more platforms can be safely associated with the same resource in the tool. However, when the resources offered by a tool are not suitable for sharing (for example, a group activity) the relationship is likely to be restricted to one-to-one, with each link in the platform being associated with its own acitvity within the tool. But there may be times when an instructor would like to have students from more than one course or from more than one platform (i.e. from more than one link) to engage together in an activity. In such cases a tool could implement a mechanism by which an instructor identifies which links are to be grouped together and this mapping used to direct students into the shared activity, but this may require giving access to data from other platforms and having a convenient mechansim for identifying the links (which may have still to be created). Thus, this library implements a simple mechanism of share keys which can be used to enable users launching from multiple links to be directed into a single resource at the tool end. The process works as follows:
- One of the resource links is chosen to be the primary connection from which the sharing is controlled;
- An instructor launched from the primary link generates a share key;
- The instructor passes the share key to an instructor of a (secondary) link to be included in the shared activity;
- The instructor adds the share key to the custom parameters for their secondary link within the platforms;
- When the secondary link is launched, the library reports the user as having launched from the primary link and so a tool will automatically allow them to share the activity;
- A share key can only be used once; a new one should be generated for each link which is to be allowed to share the primary activity.
The secondary links may exist on different platforms provided they are all configured to use the same instance of the tool. When generating a share key, an instructor may choose to automatically approve the link which uses it, or not. In the latter case, all launches from the secondary link will fail until the instructor approves it; at least one launch will be required to activate the share arrangement before it can be approved.
A share key is of the form share_key=xyz
, where xyz
is the unique value generated. A share key value may have a length of up to 32 characters. The share key custom parameter is activated on the first launch from the secondary link; thereafter its value is ignored but its presence is required to indicate that the sharing option is still required.
When a sharing arrangement is in place, in the Tool's onLaunch
method, the resoureLink
property will refer to the primary link. The resourceLink
property of the userResult
object will refer to the link from which the user actually launched. It may be necessary to keep a record of both in your application.
An example implementation of share keys can be found in the sample LTI Rating application. Note that the mechanism does depend upon a platform allowing custom parameters to be specified for individual LTI links; thus, it cannot be used with platforms such as Canvas which do not currently support this.
© 2022 Stephen P Vickers. All Rights Reserved.