-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Consumer launch is now working... next up, get Provider to respond
- Loading branch information
Showing
34 changed files
with
2,068 additions
and
44 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
<?php | ||
class Lti extends CakeMigration { | ||
|
||
/** | ||
* Migration description | ||
* | ||
* @var string | ||
*/ | ||
public $description = 'Add LTI support'; | ||
|
||
/** | ||
* Actions to be performed | ||
* | ||
* @var array $migration | ||
*/ | ||
public $migration = [ | ||
'up' => [ | ||
'create_table' => [ | ||
'lti_consumers' => [ | ||
'consumer_key' => ['type' => 'string', 'null' => false, 'length' => 255], | ||
'name' => ['type' => 'string', 'null' => false, 'length' => 45], | ||
'secret' => ['type' => 'string', 'null' => false, 'length' => 32], | ||
'lti_version' => ['type' => 'string', 'null' => true, 'length' => 12], | ||
'consumer_name' => ['type' => 'string', 'null' => true, 'length' => 255], | ||
'consumer_version' => ['type' => 'string', 'null' => true, 'length' => 255], | ||
'consumer_guid' => ['type' => 'string', 'null' => true, 'length' => 255], | ||
'css_path' => ['type' => 'string', 'null' => true, 'length' => 255], | ||
'protected' => ['type' => 'boolean', 'null' => false], | ||
'enabled' => ['type' => 'boolean', 'null' => false], | ||
'enable_from' => ['type' => 'datetime', 'null' => true], | ||
'enable_until' => ['type' => 'datetime', 'null' => true], | ||
'last_access' => ['type' => 'date', 'null' => true], | ||
'created' => ['type' => 'datetime', 'null' => false], | ||
'modified' => ['type' => 'datetime', 'null' => false], | ||
'indexes' => [ | ||
'PRIMARY' => ['unique' => true, 'column' => 'consumer_key'], | ||
], | ||
'tableParameters' => [], | ||
], | ||
'lti_contexts' => [ | ||
'consumer_key' => ['type' => 'string', 'null' => false, 'length' => 255], | ||
'context_id' => ['type' => 'string', 'null' => false, 'length' => 255], | ||
'lti_context_id' => ['type' => 'string', 'null' => true, 'length' => 255], | ||
'lti_resource_id' => ['type' => 'string', 'null' => true, 'length' => 255], | ||
'title' => ['type' => 'string', 'null' => false, 'length' => 255], | ||
'primary_consumer_key' => ['type' => 'string', 'null' => true, 'length' => 255], | ||
'primary_context_id' => ['type' => 'string', 'null' => true, 'length' => 255], | ||
'share_approved' => ['type' => 'boolean', 'null' => true], | ||
'settings' => ['type' => 'text', 'null' => true, 'length' => 1073741824], | ||
'created' => ['type' => 'datetime', 'null' => false], | ||
'modified' => ['type' => 'datetime', 'null' => false], | ||
'indexes' => [ | ||
'context_unique' => ['unique' => true, 'column' => ['consumer_key', 'context_id']], | ||
], | ||
'tableParameters' => [], | ||
], | ||
'lti_users' => [ | ||
'consumer_key' => ['type' => 'string', 'null' => false, 'length' => 255], | ||
'context_id' => ['type' => 'string', 'null' => false, 'length' => 255], | ||
'user_id' => ['type' => 'string', 'null' => false, 'length' => 255], | ||
'lti_result_sourcedid' => ['type' => 'string', 'null' => false, 'length' => 255], | ||
'created' => ['type' => 'datetime', 'null' => false], | ||
'modified' => ['type' => 'datetime', 'null' => false], | ||
'indexes' => [ | ||
'user_unique' => ['unique' => true, 'column' => ['consumer_key', 'context_id', 'user_id']], | ||
], | ||
'tableParameters' => [], | ||
], | ||
|
||
'lti_nonces' => [ | ||
'consumer_key' => ['type' => 'string', 'null' => false, 'length' => 255], | ||
'value' => ['type' => 'string', 'null' => false, 'length' => 32], | ||
'expires' => ['type' => 'datetime', 'null' => false], | ||
'indexes' => [ | ||
'nonce_unique' => ['unique' => true, 'column' => ['consumer_key', 'value']], | ||
], | ||
'tableParameters' => [], | ||
], | ||
|
||
'lti_share_keys' => [ | ||
'share_key_id' => ['type' => 'string', 'null' => false, 'length' => 32], | ||
'primary_consumer_key' => ['type' => 'string', 'null' => false, 'length' => 255], | ||
'primary_context_id' => ['type' => 'string', 'null' => false, 'length' => 255], | ||
'auto_approve' => ['type' => 'boolean', 'null' => false], | ||
'expires' => ['type' => 'datetime', 'null' => false], | ||
'indexes' => [ | ||
'PRIMARY' => ['unique' => true, 'column' => 'share_key_id'], | ||
], | ||
'tableParameters' => [], | ||
], | ||
|
||
], | ||
|
||
|
||
// 'create_field' => [ | ||
// 'core_programs' => [ | ||
// 'dashboard_content' => ['type' => 'text', 'null' => true, 'length' => 1073741824], | ||
// ], | ||
// ], | ||
], | ||
'down' => [ | ||
'drop_table' => [ | ||
'lti_consumers', 'lti_contexts', 'lti_users', 'lti_share_keys', 'lti_nonces' | ||
] | ||
], | ||
]; | ||
|
||
/** | ||
* Before migration callback | ||
* | ||
* @param string $direction Direction of migration process (up or down) | ||
* @return bool Should process continue | ||
*/ | ||
public function before($direction) { | ||
return true; | ||
} | ||
|
||
/** | ||
* After migration callback | ||
* | ||
* @param string $direction Direction of migration process (up or down) | ||
* @return bool Should process continue | ||
*/ | ||
public function after($direction) { | ||
return true; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,8 @@ | ||
<?php | ||
App::uses('LTIAppController', 'LTI.Controller'); | ||
App::uses('OAuth', 'LTI.Lib'); | ||
App::uses('LtiAppController', 'Lti.Controller'); | ||
App::import('Vendor', 'Lti.OAuth', ['file' => 'OAuth.php']); | ||
|
||
class ProvidersController extends LTIAppController { | ||
class ConsumersController extends LtiAppController { | ||
public $components = []; | ||
|
||
/** | ||
|
@@ -12,6 +12,7 @@ class ProvidersController extends LTIAppController { | |
*/ | ||
public $actions = [ | ||
'all' => [ | ||
'launch', 'response' | ||
], | ||
'admin' => [ | ||
'admin_index'/*, 'admin_add', 'admin_edit', 'admin_delete', 'admin_award', 'admin_conditions', 'admin_condition_remove' */ | ||
|
@@ -21,7 +22,7 @@ class ProvidersController extends LTIAppController { | |
]; | ||
|
||
public function beforeFilter() { | ||
$this->Security->unlockedActions = ['admin_index']; | ||
$this->Security->unlockedActions = ['admin_index', 'launch', 'response']; | ||
parent::beforeFilter(); | ||
} | ||
|
||
|
@@ -33,6 +34,159 @@ public function isAuthorized($user) { | |
public function admin_index() { | ||
} | ||
|
||
public function launch() { | ||
$launch_url = Router::url(['plugin' => 'lti', 'admin'=>false, 'controller' => 'providers', 'action' => 'request'], true); | ||
$return_url = Router::url(['plugin' => 'lti', 'admin'=>false, 'controller' => 'consumers', 'action' => 'response'], true); | ||
$outcome_url = Router::url(['plugin' => 'lti', 'admin'=>false, 'controller' => 'consumers', 'action' => 'outcome'], true); | ||
$default_lmsdata = [ | ||
"resource_link_id" => "120988f929-274612", | ||
"resource_link_title" => "Weekly Blog", | ||
"resource_link_description" => "A weekly blog.", | ||
"user_id" => "292832126", | ||
"roles" => "Instructor", // or Learner | ||
"lis_person_name_full" => 'Jane Q. Public', | ||
"lis_person_name_family" => 'Public', | ||
"lis_person_name_given" => 'Jane', | ||
"lis_person_contact_email_primary" => "[email protected]", | ||
"lis_person_sourcedid" => "school.edu:user", | ||
"context_id" => "456434513", | ||
"context_title" => "Design of Personal Environments", | ||
"context_label" => "SI182", | ||
"tool_consumer_info_product_family_code" => "ims", | ||
"tool_consumer_info_version" => "1.1", | ||
"tool_consumer_instance_guid" => "lmsng.school.edu", | ||
"tool_consumer_instance_description" => "University of School (LMSng)", | ||
"launch_presentation_locale" => "en-US", | ||
"launch_presentation_document_target" => "frame", | ||
"launch_presentation_width" => null, | ||
"launch_presentation_height" => null, | ||
"launch_presentation_css_url"=> "http://www.imsglobal.org/developers/LTI/test/v1p1/lms.css", | ||
]; | ||
|
||
$default_desc = str_replace("CUR_URL", $launch_url, | ||
'<?xml version="1.0" encoding="UTF-8"?> | ||
<basic_lti_link xmlns="http://www.imsglobal.org/services/cc/imsblti_v1p0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> | ||
<title>A Simple Descriptor</title> | ||
<custom> | ||
<parameter key="Cool:Factor">120</parameter> | ||
</custom> | ||
<launch_url>CUR_URL</launch_url> | ||
</basic_lti_link> | ||
'); | ||
|
||
$default_vars = [ | ||
'key' => "12345", | ||
'secret' => 'secret', | ||
'endpoint' => $launch_url, | ||
'urlformat' => false, | ||
'format' => '', | ||
'xmldesc' => $default_desc, | ||
]; | ||
|
||
foreach ($default_lmsdata as $k => $val ) { | ||
$lmsdata[$k] = $val; | ||
if (!empty($this->request->data['Consumer'][$k]) ) { | ||
$lmsdata[$k] = $this->request->data['Consumer'][$k]; | ||
} | ||
} | ||
|
||
foreach ($default_vars as $k => $v) { | ||
$$k = $v; | ||
if ( !empty($this->request->data['Consumer'][$k]) ) { | ||
$$k = $this->request->data['Consumer'][$k]; | ||
$default_vars[$k] = $$k; | ||
} | ||
} | ||
|
||
$urlformat = ( $format != 'XML' ); | ||
|
||
$xmldesc = str_replace("\\\"","\"",$xmldesc); | ||
|
||
$this->set(compact('lmsdata', 'key', 'secret', 'urlformat', 'endpoint', 'xmldesc')); | ||
|
||
$params = []; | ||
if ( $urlformat ) { | ||
$params = $lmsdata; | ||
} else { | ||
$cx = $this->_launchInfo($xmldesc); | ||
$endpoint = $cx["launch_url"]; | ||
|
||
if ( empty($endpoint) ) { | ||
echo("<p>Error, did not find a launch_url or secure_launch_url in the XML descriptor</p>\n"); | ||
exit(); | ||
} | ||
$custom = $cx["custom"]; | ||
$params = array_merge($lmsdata, $custom); | ||
} | ||
|
||
// Cleanup parms before we sign | ||
foreach( $params as $k => $val ) { | ||
if (strlen(trim($params[$k]) ) < 1 ) { | ||
unset($params[$k]); | ||
} | ||
} | ||
|
||
// Add oauth_callback to be compliant with the 1.0A spec | ||
$params["oauth_callback"] = "about:blank"; | ||
$params["lti_version"] = "LTI-1p0"; | ||
$params["lti_message_type"] = "basic-lti-launch-request"; | ||
$params["launch_presentation_return_url"] = $return_url; | ||
$params["lis_outcome_service_url"] = $outcome_url; | ||
$params["lis_result_sourcedid"] = "feb-123-456-2929::28883"; | ||
$params = $this->_signLaunchParameters($endpoint, "POST", $params, $key, $secret); | ||
// ksort($params); | ||
// pr($params); | ||
$params["ext_submit"] = "Press to Launch"; // this is so we can inject launch buttons with custom text | ||
$this->set("iframeattr", "width=\"100%\" height=\"900\" scrolling=\"auto\" frameborder=\"1\" transparency"); | ||
$this->set("params", $params); | ||
$this->request->data['Consumer'] = am($lmsdata, $default_vars, $params); | ||
} | ||
|
||
public function response() { | ||
$this->layout = 'basic'; | ||
foreach (['lti_msg', 'lti_errormsg', 'lti_log', 'lti_errorlog'] as $var) { | ||
$$var = ''; | ||
if (!empty($this->request->query[$var])) { | ||
$$var = $this->request->query[$var]; | ||
} | ||
$this->set($var, $$var); | ||
} | ||
} | ||
|
||
// Parse a descriptor | ||
protected function _launchInfo($xmldata) { | ||
$xml = new SimpleXMLElement($xmldata); | ||
if ( empty($xml) ) { | ||
echo("Error parsing Descriptor XML\n"); | ||
return; | ||
} | ||
$launch_url = $xml->secure_launch_url[0]; | ||
|
||
if ( empty($launch_url) ) { | ||
$launch_url = $xml->launch_url[0]; | ||
} | ||
|
||
$launch_url = (string) $launch_url; | ||
|
||
$custom = array(); | ||
if ( !empty($xml->custom[0]->parameter ) ) { | ||
foreach ( $xml->custom[0]->parameter as $resource) { | ||
$key = (string) $resource['key']; | ||
$key = strtolower($key); | ||
$nk = ""; | ||
for($i=0; $i < strlen($key); $i++) { | ||
$ch = substr($key,$i,1); | ||
if ( $ch >= "a" && $ch <= "z" ) $nk .= $ch; | ||
else if ( $ch >= "0" && $ch <= "9" ) $nk .= $ch; | ||
else $nk .= "_"; | ||
} | ||
$value = (string) $resource; | ||
$custom["custom_".$nk] = $value; | ||
} | ||
} | ||
return [ "launch_url" => $launch_url, "custom" => $custom ] ; | ||
} | ||
|
||
|
||
/** | ||
* Is the consumer key available to accept launch requests? | ||
|
@@ -65,7 +219,7 @@ public function getIsAvailable() { | |
* | ||
* @return array Array of signed message parameters | ||
*/ | ||
public function signParameters($url, $type, $version, $params) { | ||
protected function _signParameters($url, $type, $version, $params) { | ||
|
||
if (!empty($url)) { | ||
// Check for query parameters which need to be included in the signature | ||
|
@@ -103,6 +257,25 @@ public function signParameters($url, $type, $version, $params) { | |
|
||
} | ||
|
||
protected function _signLaunchParameters($endpoint, $method="POST", $params, $oauth_consumer_key, $oauth_consumer_secret) | ||
{ | ||
|
||
$token = null; | ||
|
||
$hmac_method = new OAuthSignatureMethod_HMAC_SHA1(); | ||
$consumer = new OAuthConsumer($oauth_consumer_key, $oauth_consumer_secret, NULL); | ||
|
||
$acc_req = OAuthRequest::from_consumer_and_token($consumer, $token, $method, $endpoint, $params); | ||
$acc_req->sign_request($hmac_method, $consumer, $token); | ||
|
||
// Pass this back up "out of band" for debugging | ||
$last_base_string = $acc_req->get_signature_base_string(); | ||
$this->set('last_base_string', $last_base_string); | ||
$params = $acc_req->get_parameters(); | ||
|
||
return $params; | ||
} | ||
|
||
### | ||
### PRIVATE METHOD | ||
### | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
<?php | ||
App::uses('AppController', 'Controller'); | ||
|
||
class LTIAppController extends AppController { | ||
class LtiAppController extends AppController { | ||
public $components = []; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.