1 |
efrain |
1 |
<?php
|
|
|
2 |
|
|
|
3 |
namespace IMSGlobal\LTI\ToolProvider;
|
|
|
4 |
|
|
|
5 |
use IMSGlobal\LTI\Profile\Item;
|
|
|
6 |
use IMSGlobal\LTI\ToolProvider\DataConnector\DataConnector;
|
|
|
7 |
use IMSGlobal\LTI\ToolProvider\MediaType;
|
|
|
8 |
use IMSGlobal\LTI\Profile;
|
|
|
9 |
use IMSGlobal\LTI\HTTPMessage;
|
|
|
10 |
use IMSGlobal\LTI\OAuth;
|
|
|
11 |
|
|
|
12 |
/**
|
|
|
13 |
* Class to represent an LTI Tool Provider
|
|
|
14 |
*
|
|
|
15 |
* @author Stephen P Vickers <svickers@imsglobal.org>
|
|
|
16 |
* @copyright IMS Global Learning Consortium Inc
|
|
|
17 |
* @date 2016
|
|
|
18 |
* @version 3.0.2
|
|
|
19 |
* @license GNU Lesser General Public License, version 3 (<http://www.gnu.org/licenses/lgpl.html>)
|
|
|
20 |
*/
|
|
|
21 |
#[\AllowDynamicProperties]
|
|
|
22 |
class ToolProvider
|
|
|
23 |
{
|
|
|
24 |
|
|
|
25 |
/**
|
|
|
26 |
* Default connection error message.
|
|
|
27 |
*/
|
|
|
28 |
const CONNECTION_ERROR_MESSAGE = 'Sorry, there was an error connecting you to the application.';
|
|
|
29 |
|
|
|
30 |
/**
|
|
|
31 |
* LTI version 1 for messages.
|
|
|
32 |
*/
|
|
|
33 |
const LTI_VERSION1 = 'LTI-1p0';
|
|
|
34 |
/**
|
|
|
35 |
* LTI version 2 for messages.
|
|
|
36 |
*/
|
|
|
37 |
const LTI_VERSION2 = 'LTI-2p0';
|
|
|
38 |
/**
|
|
|
39 |
* Use ID value only.
|
|
|
40 |
*/
|
|
|
41 |
const ID_SCOPE_ID_ONLY = 0;
|
|
|
42 |
/**
|
|
|
43 |
* Prefix an ID with the consumer key.
|
|
|
44 |
*/
|
|
|
45 |
const ID_SCOPE_GLOBAL = 1;
|
|
|
46 |
/**
|
|
|
47 |
* Prefix the ID with the consumer key and context ID.
|
|
|
48 |
*/
|
|
|
49 |
const ID_SCOPE_CONTEXT = 2;
|
|
|
50 |
/**
|
|
|
51 |
* Prefix the ID with the consumer key and resource ID.
|
|
|
52 |
*/
|
|
|
53 |
const ID_SCOPE_RESOURCE = 3;
|
|
|
54 |
/**
|
|
|
55 |
* Character used to separate each element of an ID.
|
|
|
56 |
*/
|
|
|
57 |
const ID_SCOPE_SEPARATOR = ':';
|
|
|
58 |
|
|
|
59 |
/**
|
|
|
60 |
* Permitted LTI versions for messages.
|
|
|
61 |
*/
|
|
|
62 |
private static $LTI_VERSIONS = array(self::LTI_VERSION1, self::LTI_VERSION2);
|
|
|
63 |
/**
|
|
|
64 |
* List of supported message types and associated class methods.
|
|
|
65 |
*/
|
|
|
66 |
private static $MESSAGE_TYPES = array('basic-lti-launch-request' => 'onLaunch',
|
|
|
67 |
'ContentItemSelectionRequest' => 'onContentItem',
|
|
|
68 |
'ToolProxyRegistrationRequest' => 'register');
|
|
|
69 |
/**
|
|
|
70 |
* List of supported message types and associated class methods
|
|
|
71 |
*
|
|
|
72 |
* @var array $METHOD_NAMES
|
|
|
73 |
*/
|
|
|
74 |
private static $METHOD_NAMES = array('basic-lti-launch-request' => 'onLaunch',
|
|
|
75 |
'ContentItemSelectionRequest' => 'onContentItem',
|
|
|
76 |
'ToolProxyRegistrationRequest' => 'onRegister');
|
|
|
77 |
/**
|
|
|
78 |
* Names of LTI parameters to be retained in the consumer settings property.
|
|
|
79 |
*
|
|
|
80 |
* @var array $LTI_CONSUMER_SETTING_NAMES
|
|
|
81 |
*/
|
|
|
82 |
private static $LTI_CONSUMER_SETTING_NAMES = array('custom_tc_profile_url', 'custom_system_setting_url');
|
|
|
83 |
/**
|
|
|
84 |
* Names of LTI parameters to be retained in the context settings property.
|
|
|
85 |
*
|
|
|
86 |
* @var array $LTI_CONTEXT_SETTING_NAMES
|
|
|
87 |
*/
|
|
|
88 |
private static $LTI_CONTEXT_SETTING_NAMES = array('custom_context_setting_url',
|
|
|
89 |
'custom_lineitems_url', 'custom_results_url',
|
|
|
90 |
'custom_context_memberships_url');
|
|
|
91 |
/**
|
|
|
92 |
* Names of LTI parameters to be retained in the resource link settings property.
|
|
|
93 |
*
|
|
|
94 |
* @var array $LTI_RESOURCE_LINK_SETTING_NAMES
|
|
|
95 |
*/
|
|
|
96 |
private static $LTI_RESOURCE_LINK_SETTING_NAMES = array('lis_course_section_sourcedid', 'lis_result_sourcedid', 'lis_outcome_service_url',
|
|
|
97 |
'ext_ims_lis_basic_outcome_url', 'ext_ims_lis_resultvalue_sourcedids',
|
|
|
98 |
'ext_ims_lis_memberships_id', 'ext_ims_lis_memberships_url',
|
|
|
99 |
'ext_ims_lti_tool_setting', 'ext_ims_lti_tool_setting_id', 'ext_ims_lti_tool_setting_url',
|
|
|
100 |
'custom_link_setting_url',
|
|
|
101 |
'custom_lineitem_url', 'custom_result_url');
|
|
|
102 |
/**
|
|
|
103 |
* Names of LTI custom parameter substitution variables (or capabilities) and their associated default message parameter names.
|
|
|
104 |
*
|
|
|
105 |
* @var array $CUSTOM_SUBSTITUTION_VARIABLES
|
|
|
106 |
*/
|
|
|
107 |
private static $CUSTOM_SUBSTITUTION_VARIABLES = array('User.id' => 'user_id',
|
|
|
108 |
'User.image' => 'user_image',
|
|
|
109 |
'User.username' => 'username',
|
|
|
110 |
'User.scope.mentor' => 'role_scope_mentor',
|
|
|
111 |
'Membership.role' => 'roles',
|
|
|
112 |
'Person.sourcedId' => 'lis_person_sourcedid',
|
|
|
113 |
'Person.name.full' => 'lis_person_name_full',
|
|
|
114 |
'Person.name.family' => 'lis_person_name_family',
|
|
|
115 |
'Person.name.given' => 'lis_person_name_given',
|
|
|
116 |
'Person.email.primary' => 'lis_person_contact_email_primary',
|
|
|
117 |
'Context.id' => 'context_id',
|
|
|
118 |
'Context.type' => 'context_type',
|
|
|
119 |
'Context.title' => 'context_title',
|
|
|
120 |
'Context.label' => 'context_label',
|
|
|
121 |
'CourseOffering.sourcedId' => 'lis_course_offering_sourcedid',
|
|
|
122 |
'CourseSection.sourcedId' => 'lis_course_section_sourcedid',
|
|
|
123 |
'CourseSection.label' => 'context_label',
|
|
|
124 |
'CourseSection.title' => 'context_title',
|
|
|
125 |
'ResourceLink.id' => 'resource_link_id',
|
|
|
126 |
'ResourceLink.title' => 'resource_link_title',
|
|
|
127 |
'ResourceLink.description' => 'resource_link_description',
|
|
|
128 |
'Result.sourcedId' => 'lis_result_sourcedid',
|
|
|
129 |
'BasicOutcome.url' => 'lis_outcome_service_url',
|
|
|
130 |
'ToolConsumerProfile.url' => 'custom_tc_profile_url',
|
|
|
131 |
'ToolProxy.url' => 'tool_proxy_url',
|
|
|
132 |
'ToolProxy.custom.url' => 'custom_system_setting_url',
|
|
|
133 |
'ToolProxyBinding.custom.url' => 'custom_context_setting_url',
|
|
|
134 |
'LtiLink.custom.url' => 'custom_link_setting_url',
|
|
|
135 |
'LineItems.url' => 'custom_lineitems_url',
|
|
|
136 |
'LineItem.url' => 'custom_lineitem_url',
|
|
|
137 |
'Results.url' => 'custom_results_url',
|
|
|
138 |
'Result.url' => 'custom_result_url',
|
|
|
139 |
'ToolProxyBinding.memberships.url' => 'custom_context_memberships_url');
|
|
|
140 |
|
|
|
141 |
|
|
|
142 |
/**
|
|
|
143 |
* True if the last request was successful.
|
|
|
144 |
*
|
|
|
145 |
* @var boolean $ok
|
|
|
146 |
*/
|
|
|
147 |
public $ok = true;
|
|
|
148 |
/**
|
|
|
149 |
* Tool Consumer object.
|
|
|
150 |
*
|
|
|
151 |
* @var ToolConsumer $consumer
|
|
|
152 |
*/
|
|
|
153 |
public $consumer = null;
|
|
|
154 |
/**
|
|
|
155 |
* Return URL provided by tool consumer.
|
|
|
156 |
*
|
|
|
157 |
* @var string $returnUrl
|
|
|
158 |
*/
|
|
|
159 |
public $returnUrl = null;
|
|
|
160 |
/**
|
|
|
161 |
* User object.
|
|
|
162 |
*
|
|
|
163 |
* @var User $user
|
|
|
164 |
*/
|
|
|
165 |
public $user = null;
|
|
|
166 |
/**
|
|
|
167 |
* Resource link object.
|
|
|
168 |
*
|
|
|
169 |
* @var ResourceLink $resourceLink
|
|
|
170 |
*/
|
|
|
171 |
public $resourceLink = null;
|
|
|
172 |
/**
|
|
|
173 |
* Context object.
|
|
|
174 |
*
|
|
|
175 |
* @var Context $context
|
|
|
176 |
*/
|
|
|
177 |
public $context = null;
|
|
|
178 |
/**
|
|
|
179 |
* Data connector object.
|
|
|
180 |
*
|
|
|
181 |
* @var DataConnector $dataConnector
|
|
|
182 |
*/
|
|
|
183 |
public $dataConnector = null;
|
|
|
184 |
/**
|
|
|
185 |
* Default email domain.
|
|
|
186 |
*
|
|
|
187 |
* @var string $defaultEmail
|
|
|
188 |
*/
|
|
|
189 |
public $defaultEmail = '';
|
|
|
190 |
/**
|
|
|
191 |
* Scope to use for user IDs.
|
|
|
192 |
*
|
|
|
193 |
* @var int $idScope
|
|
|
194 |
*/
|
|
|
195 |
public $idScope = self::ID_SCOPE_ID_ONLY;
|
|
|
196 |
/**
|
|
|
197 |
* Whether shared resource link arrangements are permitted.
|
|
|
198 |
*
|
|
|
199 |
* @var boolean $allowSharing
|
|
|
200 |
*/
|
|
|
201 |
public $allowSharing = false;
|
|
|
202 |
/**
|
|
|
203 |
* Message for last request processed
|
|
|
204 |
*
|
|
|
205 |
* @var string $message
|
|
|
206 |
*/
|
|
|
207 |
public $message = self::CONNECTION_ERROR_MESSAGE;
|
|
|
208 |
/**
|
|
|
209 |
* Error message for last request processed.
|
|
|
210 |
*
|
|
|
211 |
* @var string $reason
|
|
|
212 |
*/
|
|
|
213 |
public $reason = null;
|
|
|
214 |
/**
|
|
|
215 |
* Details for error message relating to last request processed.
|
|
|
216 |
*
|
|
|
217 |
* @var array $details
|
|
|
218 |
*/
|
|
|
219 |
public $details = array();
|
|
|
220 |
/**
|
|
|
221 |
* Base URL for tool provider service
|
|
|
222 |
*
|
|
|
223 |
* @var string $baseUrl
|
|
|
224 |
*/
|
|
|
225 |
public $baseUrl = null;
|
|
|
226 |
/**
|
|
|
227 |
* Vendor details
|
|
|
228 |
*
|
|
|
229 |
* @var Item $vendor
|
|
|
230 |
*/
|
|
|
231 |
public $vendor = null;
|
|
|
232 |
/**
|
|
|
233 |
* Product details
|
|
|
234 |
*
|
|
|
235 |
* @var Item $product
|
|
|
236 |
*/
|
|
|
237 |
public $product = null;
|
|
|
238 |
/**
|
|
|
239 |
* Services required by Tool Provider
|
|
|
240 |
*
|
|
|
241 |
* @var array $requiredServices
|
|
|
242 |
*/
|
|
|
243 |
public $requiredServices = null;
|
|
|
244 |
/**
|
|
|
245 |
* Optional services used by Tool Provider
|
|
|
246 |
*
|
|
|
247 |
* @var array $optionalServices
|
|
|
248 |
*/
|
|
|
249 |
public $optionalServices = null;
|
|
|
250 |
/**
|
|
|
251 |
* Resource handlers for Tool Provider
|
|
|
252 |
*
|
|
|
253 |
* @var array $resourceHandlers
|
|
|
254 |
*/
|
|
|
255 |
public $resourceHandlers = null;
|
|
|
256 |
|
|
|
257 |
/**
|
|
|
258 |
* URL to redirect user to on successful completion of the request.
|
|
|
259 |
*
|
|
|
260 |
* @var string $redirectUrl
|
|
|
261 |
*/
|
|
|
262 |
protected $redirectUrl = null;
|
|
|
263 |
/**
|
|
|
264 |
* URL to redirect user to on successful completion of the request.
|
|
|
265 |
*
|
|
|
266 |
* @var string $mediaTypes
|
|
|
267 |
*/
|
|
|
268 |
protected $mediaTypes = null;
|
|
|
269 |
/**
|
|
|
270 |
* URL to redirect user to on successful completion of the request.
|
|
|
271 |
*
|
|
|
272 |
* @var string $documentTargets
|
|
|
273 |
*/
|
|
|
274 |
protected $documentTargets = null;
|
|
|
275 |
/**
|
|
|
276 |
* HTML to be displayed on a successful completion of the request.
|
|
|
277 |
*
|
|
|
278 |
* @var string $output
|
|
|
279 |
*/
|
|
|
280 |
protected $output = null;
|
|
|
281 |
/**
|
|
|
282 |
* HTML to be displayed on an unsuccessful completion of the request and no return URL is available.
|
|
|
283 |
*
|
|
|
284 |
* @var string $errorOutput
|
|
|
285 |
*/
|
|
|
286 |
protected $errorOutput = null;
|
|
|
287 |
/**
|
|
|
288 |
* Whether debug messages explaining the cause of errors are to be returned to the tool consumer.
|
|
|
289 |
*
|
|
|
290 |
* @var boolean $debugMode
|
|
|
291 |
*/
|
|
|
292 |
protected $debugMode = false;
|
|
|
293 |
|
|
|
294 |
/**
|
|
|
295 |
* Callback functions for handling requests.
|
|
|
296 |
*
|
|
|
297 |
* @var array $callbackHandler
|
|
|
298 |
*/
|
|
|
299 |
private $callbackHandler = null;
|
|
|
300 |
/**
|
|
|
301 |
* LTI parameter constraints for auto validation checks.
|
|
|
302 |
*
|
|
|
303 |
* @var array $constraints
|
|
|
304 |
*/
|
|
|
305 |
private $constraints = null;
|
|
|
306 |
|
|
|
307 |
/**
|
|
|
308 |
* Class constructor
|
|
|
309 |
*
|
|
|
310 |
* @param DataConnector $dataConnector Object containing a database connection object
|
|
|
311 |
*/
|
|
|
312 |
function __construct($dataConnector)
|
|
|
313 |
{
|
|
|
314 |
|
|
|
315 |
$this->constraints = array();
|
|
|
316 |
$this->dataConnector = $dataConnector;
|
|
|
317 |
$this->ok = !is_null($this->dataConnector);
|
|
|
318 |
|
|
|
319 |
// Set debug mode
|
|
|
320 |
$this->debugMode = isset($_POST['custom_debug']) && (strtolower($_POST['custom_debug']) === 'true');
|
|
|
321 |
|
|
|
322 |
// Set return URL if available
|
|
|
323 |
if (isset($_POST['launch_presentation_return_url'])) {
|
|
|
324 |
$this->returnUrl = $_POST['launch_presentation_return_url'];
|
|
|
325 |
} else if (isset($_POST['content_item_return_url'])) {
|
|
|
326 |
$this->returnUrl = $_POST['content_item_return_url'];
|
|
|
327 |
}
|
|
|
328 |
$this->vendor = new Profile\Item();
|
|
|
329 |
$this->product = new Profile\Item();
|
|
|
330 |
$this->requiredServices = array();
|
|
|
331 |
$this->optionalServices = array();
|
|
|
332 |
$this->resourceHandlers = array();
|
|
|
333 |
|
|
|
334 |
}
|
|
|
335 |
|
|
|
336 |
/**
|
|
|
337 |
* Process an incoming request
|
|
|
338 |
*/
|
|
|
339 |
public function handleRequest()
|
|
|
340 |
{
|
|
|
341 |
|
|
|
342 |
if ($this->ok) {
|
|
|
343 |
if ($this->authenticate()) {
|
|
|
344 |
$this->doCallback();
|
|
|
345 |
}
|
|
|
346 |
}
|
|
|
347 |
$this->result();
|
|
|
348 |
|
|
|
349 |
}
|
|
|
350 |
|
|
|
351 |
/**
|
|
|
352 |
* Add a parameter constraint to be checked on launch
|
|
|
353 |
*
|
|
|
354 |
* @param string $name Name of parameter to be checked
|
|
|
355 |
* @param boolean $required True if parameter is required (optional, default is true)
|
|
|
356 |
* @param int $maxLength Maximum permitted length of parameter value (optional, default is null)
|
|
|
357 |
* @param array $messageTypes Array of message types to which the constraint applies (optional, default is all)
|
|
|
358 |
*/
|
|
|
359 |
public function setParameterConstraint($name, $required = true, $maxLength = null, $messageTypes = null)
|
|
|
360 |
{
|
|
|
361 |
|
|
|
362 |
$name = trim($name);
|
|
|
363 |
if (strlen($name) > 0) {
|
|
|
364 |
$this->constraints[$name] = array('required' => $required, 'max_length' => $maxLength, 'messages' => $messageTypes);
|
|
|
365 |
}
|
|
|
366 |
|
|
|
367 |
}
|
|
|
368 |
|
|
|
369 |
/**
|
|
|
370 |
* Get an array of defined tool consumers
|
|
|
371 |
*
|
|
|
372 |
* @return array Array of ToolConsumer objects
|
|
|
373 |
*/
|
|
|
374 |
public function getConsumers()
|
|
|
375 |
{
|
|
|
376 |
|
|
|
377 |
return $this->dataConnector->getToolConsumers();
|
|
|
378 |
|
|
|
379 |
}
|
|
|
380 |
|
|
|
381 |
/**
|
|
|
382 |
* Find an offered service based on a media type and HTTP action(s)
|
|
|
383 |
*
|
|
|
384 |
* @param string $format Media type required
|
|
|
385 |
* @param array $methods Array of HTTP actions required
|
|
|
386 |
*
|
|
|
387 |
* @return object The service object
|
|
|
388 |
*/
|
|
|
389 |
public function findService($format, $methods)
|
|
|
390 |
{
|
|
|
391 |
|
|
|
392 |
$found = false;
|
|
|
393 |
$services = $this->consumer->profile->service_offered;
|
|
|
394 |
if (is_array($services)) {
|
|
|
395 |
$n = -1;
|
|
|
396 |
foreach ($services as $service) {
|
|
|
397 |
$n++;
|
|
|
398 |
if (!is_array($service->format) || !in_array($format, $service->format)) {
|
|
|
399 |
continue;
|
|
|
400 |
}
|
|
|
401 |
$missing = array();
|
|
|
402 |
foreach ($methods as $method) {
|
|
|
403 |
if (!is_array($service->action) || !in_array($method, $service->action)) {
|
|
|
404 |
$missing[] = $method;
|
|
|
405 |
}
|
|
|
406 |
}
|
|
|
407 |
$methods = $missing;
|
|
|
408 |
if (count($methods) <= 0) {
|
|
|
409 |
$found = $service;
|
|
|
410 |
break;
|
|
|
411 |
}
|
|
|
412 |
}
|
|
|
413 |
}
|
|
|
414 |
|
|
|
415 |
return $found;
|
|
|
416 |
|
|
|
417 |
}
|
|
|
418 |
|
|
|
419 |
/**
|
|
|
420 |
* Send the tool proxy to the Tool Consumer
|
|
|
421 |
*
|
|
|
422 |
* @return boolean True if the tool proxy was accepted
|
|
|
423 |
*/
|
|
|
424 |
public function doToolProxyService()
|
|
|
425 |
{
|
|
|
426 |
|
|
|
427 |
// Create tool proxy
|
|
|
428 |
$toolProxyService = $this->findService('application/vnd.ims.lti.v2.toolproxy+json', array('POST'));
|
|
|
429 |
$secret = DataConnector::getRandomString(12);
|
|
|
430 |
$toolProxy = new MediaType\ToolProxy($this, $toolProxyService, $secret);
|
|
|
431 |
$http = $this->consumer->doServiceRequest($toolProxyService, 'POST', 'application/vnd.ims.lti.v2.toolproxy+json', json_encode($toolProxy));
|
|
|
432 |
$ok = $http->ok && ($http->status == 201) && isset($http->responseJson->tool_proxy_guid) && (strlen($http->responseJson->tool_proxy_guid) > 0);
|
|
|
433 |
if ($ok) {
|
|
|
434 |
$this->consumer->setKey($http->responseJson->tool_proxy_guid);
|
|
|
435 |
$this->consumer->secret = $toolProxy->security_contract->shared_secret;
|
|
|
436 |
$this->consumer->toolProxy = json_encode($toolProxy);
|
|
|
437 |
$this->consumer->save();
|
|
|
438 |
}
|
|
|
439 |
|
|
|
440 |
return $ok;
|
|
|
441 |
|
|
|
442 |
}
|
|
|
443 |
|
|
|
444 |
/**
|
|
|
445 |
* Get an array of fully qualified user roles
|
|
|
446 |
*
|
|
|
447 |
* @param mixed $roles Comma-separated list of roles or array of roles
|
|
|
448 |
*
|
|
|
449 |
* @return array Array of roles
|
|
|
450 |
*/
|
|
|
451 |
public static function parseRoles($roles)
|
|
|
452 |
{
|
|
|
453 |
|
|
|
454 |
if (!is_array($roles)) {
|
|
|
455 |
$roles = explode(',', $roles);
|
|
|
456 |
}
|
|
|
457 |
$parsedRoles = array();
|
|
|
458 |
foreach ($roles as $role) {
|
|
|
459 |
$role = trim($role);
|
|
|
460 |
if (!empty($role)) {
|
|
|
461 |
if (substr($role, 0, 4) !== 'urn:') {
|
|
|
462 |
$role = 'urn:lti:role:ims/lis/' . $role;
|
|
|
463 |
}
|
|
|
464 |
$parsedRoles[] = $role;
|
|
|
465 |
}
|
|
|
466 |
}
|
|
|
467 |
|
|
|
468 |
return $parsedRoles;
|
|
|
469 |
|
|
|
470 |
}
|
|
|
471 |
|
|
|
472 |
/**
|
|
|
473 |
* Generate a web page containing an auto-submitted form of parameters.
|
|
|
474 |
*
|
|
|
475 |
* @param string $url URL to which the form should be submitted
|
|
|
476 |
* @param array $params Array of form parameters
|
|
|
477 |
* @param string $target Name of target (optional)
|
|
|
478 |
* @return string
|
|
|
479 |
*/
|
|
|
480 |
public static function sendForm($url, $params, $target = '')
|
|
|
481 |
{
|
|
|
482 |
|
|
|
483 |
$page = <<< EOD
|
|
|
484 |
<html>
|
|
|
485 |
<head>
|
|
|
486 |
<title>IMS LTI message</title>
|
|
|
487 |
<script type="text/javascript">
|
|
|
488 |
//<![CDATA[
|
|
|
489 |
function doOnLoad() {
|
|
|
490 |
document.forms[0].submit();
|
|
|
491 |
}
|
|
|
492 |
|
|
|
493 |
window.onload=doOnLoad;
|
|
|
494 |
//]]>
|
|
|
495 |
</script>
|
|
|
496 |
</head>
|
|
|
497 |
<body>
|
|
|
498 |
<form action="{$url}" method="post" target="" encType="application/x-www-form-urlencoded">
|
|
|
499 |
|
|
|
500 |
EOD;
|
|
|
501 |
|
|
|
502 |
foreach($params as $key => $value ) {
|
|
|
503 |
$key = htmlentities($key, ENT_COMPAT | ENT_HTML401, 'UTF-8');
|
|
|
504 |
$value = htmlentities($value, ENT_COMPAT | ENT_HTML401, 'UTF-8');
|
|
|
505 |
$page .= <<< EOD
|
|
|
506 |
<input type="hidden" name="{$key}" value="{$value}" />
|
|
|
507 |
|
|
|
508 |
EOD;
|
|
|
509 |
|
|
|
510 |
}
|
|
|
511 |
|
|
|
512 |
$page .= <<< EOD
|
|
|
513 |
</form>
|
|
|
514 |
</body>
|
|
|
515 |
</html>
|
|
|
516 |
EOD;
|
|
|
517 |
|
|
|
518 |
return $page;
|
|
|
519 |
|
|
|
520 |
}
|
|
|
521 |
|
|
|
522 |
###
|
|
|
523 |
### PROTECTED METHODS
|
|
|
524 |
###
|
|
|
525 |
|
|
|
526 |
/**
|
|
|
527 |
* Process a valid launch request
|
|
|
528 |
*
|
|
|
529 |
* @return boolean True if no error
|
|
|
530 |
*/
|
|
|
531 |
protected function onLaunch()
|
|
|
532 |
{
|
|
|
533 |
|
|
|
534 |
$this->onError();
|
|
|
535 |
|
|
|
536 |
}
|
|
|
537 |
|
|
|
538 |
/**
|
|
|
539 |
* Process a valid content-item request
|
|
|
540 |
*
|
|
|
541 |
* @return boolean True if no error
|
|
|
542 |
*/
|
|
|
543 |
protected function onContentItem()
|
|
|
544 |
{
|
|
|
545 |
|
|
|
546 |
$this->onError();
|
|
|
547 |
|
|
|
548 |
}
|
|
|
549 |
|
|
|
550 |
/**
|
|
|
551 |
* Process a valid tool proxy registration request
|
|
|
552 |
*
|
|
|
553 |
* @return boolean True if no error
|
|
|
554 |
*/
|
|
|
555 |
protected function onRegister() {
|
|
|
556 |
|
|
|
557 |
$this->onError();
|
|
|
558 |
|
|
|
559 |
}
|
|
|
560 |
|
|
|
561 |
/**
|
|
|
562 |
* Process a response to an invalid request
|
|
|
563 |
*
|
|
|
564 |
* @return boolean True if no further error processing required
|
|
|
565 |
*/
|
|
|
566 |
protected function onError()
|
|
|
567 |
{
|
|
|
568 |
|
|
|
569 |
$this->doCallback('onError');
|
|
|
570 |
|
|
|
571 |
}
|
|
|
572 |
|
|
|
573 |
###
|
|
|
574 |
### PRIVATE METHODS
|
|
|
575 |
###
|
|
|
576 |
|
|
|
577 |
/**
|
|
|
578 |
* Call any callback function for the requested action.
|
|
|
579 |
*
|
|
|
580 |
* This function may set the redirect_url and output properties.
|
|
|
581 |
*
|
|
|
582 |
* @return boolean True if no error reported
|
|
|
583 |
*/
|
|
|
584 |
private function doCallback($method = null)
|
|
|
585 |
{
|
|
|
586 |
|
|
|
587 |
$callback = $method;
|
|
|
588 |
if (is_null($callback)) {
|
|
|
589 |
$callback = self::$METHOD_NAMES[$_POST['lti_message_type']];
|
|
|
590 |
}
|
|
|
591 |
if (method_exists($this, $callback)) {
|
|
|
592 |
$result = $this->$callback();
|
|
|
593 |
} else if (is_null($method) && $this->ok) {
|
|
|
594 |
$this->ok = false;
|
|
|
595 |
$this->reason = "Message type not supported: {$_POST['lti_message_type']}";
|
|
|
596 |
}
|
|
|
597 |
if ($this->ok && ($_POST['lti_message_type'] == 'ToolProxyRegistrationRequest')) {
|
|
|
598 |
$this->consumer->save();
|
|
|
599 |
}
|
|
|
600 |
|
|
|
601 |
}
|
|
|
602 |
|
|
|
603 |
/**
|
|
|
604 |
* Perform the result of an action.
|
|
|
605 |
*
|
|
|
606 |
* This function may redirect the user to another URL rather than returning a value.
|
|
|
607 |
*
|
|
|
608 |
* @return string Output to be displayed (redirection, or display HTML or message)
|
|
|
609 |
*/
|
|
|
610 |
private function result()
|
|
|
611 |
{
|
|
|
612 |
|
|
|
613 |
$ok = false;
|
|
|
614 |
if (!$this->ok) {
|
|
|
615 |
$ok = $this->onError();
|
|
|
616 |
}
|
|
|
617 |
if (!$ok) {
|
|
|
618 |
if (!$this->ok) {
|
|
|
619 |
|
|
|
620 |
// If not valid, return an error message to the tool consumer if a return URL is provided
|
|
|
621 |
if (!empty($this->returnUrl)) {
|
|
|
622 |
$errorUrl = $this->returnUrl;
|
|
|
623 |
if (strpos($errorUrl, '?') === false) {
|
|
|
624 |
$errorUrl .= '?';
|
|
|
625 |
} else {
|
|
|
626 |
$errorUrl .= '&';
|
|
|
627 |
}
|
|
|
628 |
if ($this->debugMode && !is_null($this->reason)) {
|
|
|
629 |
$errorUrl .= 'lti_errormsg=' . urlencode("Debug error: $this->reason");
|
|
|
630 |
} else {
|
|
|
631 |
$errorUrl .= 'lti_errormsg=' . urlencode($this->message);
|
|
|
632 |
if (!is_null($this->reason)) {
|
|
|
633 |
$errorUrl .= '<i_errorlog=' . urlencode("Debug error: $this->reason");
|
|
|
634 |
}
|
|
|
635 |
}
|
|
|
636 |
if (!is_null($this->consumer) && isset($_POST['lti_message_type']) && ($_POST['lti_message_type'] === 'ContentItemSelectionRequest')) {
|
|
|
637 |
$formParams = array();
|
|
|
638 |
if (isset($_POST['data'])) {
|
|
|
639 |
$formParams['data'] = $_POST['data'];
|
|
|
640 |
}
|
|
|
641 |
$version = (isset($_POST['lti_version'])) ? $_POST['lti_version'] : self::LTI_VERSION1;
|
|
|
642 |
$formParams = $this->consumer->signParameters($errorUrl, 'ContentItemSelection', $version, $formParams);
|
|
|
643 |
$page = self::sendForm($errorUrl, $formParams);
|
|
|
644 |
echo $page;
|
|
|
645 |
} else {
|
|
|
646 |
header("Location: {$errorUrl}");
|
|
|
647 |
}
|
|
|
648 |
exit;
|
|
|
649 |
} else {
|
|
|
650 |
if (!is_null($this->errorOutput)) {
|
|
|
651 |
echo $this->errorOutput;
|
|
|
652 |
} else if ($this->debugMode && !empty($this->reason)) {
|
|
|
653 |
echo "Debug error: {$this->reason}";
|
|
|
654 |
} else {
|
|
|
655 |
echo "Error: {$this->message}";
|
|
|
656 |
}
|
|
|
657 |
}
|
|
|
658 |
} else if (!is_null($this->redirectUrl)) {
|
|
|
659 |
header("Location: {$this->redirectUrl}");
|
|
|
660 |
exit;
|
|
|
661 |
} else if (!is_null($this->output)) {
|
|
|
662 |
echo $this->output;
|
|
|
663 |
}
|
|
|
664 |
}
|
|
|
665 |
|
|
|
666 |
}
|
|
|
667 |
|
|
|
668 |
/**
|
|
|
669 |
* Check the authenticity of the LTI launch request.
|
|
|
670 |
*
|
|
|
671 |
* The consumer, resource link and user objects will be initialised if the request is valid.
|
|
|
672 |
*
|
|
|
673 |
* @return boolean True if the request has been successfully validated.
|
|
|
674 |
*/
|
|
|
675 |
private function authenticate()
|
|
|
676 |
{
|
|
|
677 |
|
|
|
678 |
// Get the consumer
|
|
|
679 |
$doSaveConsumer = false;
|
|
|
680 |
// Check all required launch parameters
|
|
|
681 |
$this->ok = isset($_POST['lti_message_type']) && array_key_exists($_POST['lti_message_type'], self::$MESSAGE_TYPES);
|
|
|
682 |
if (!$this->ok) {
|
|
|
683 |
$this->reason = 'Invalid or missing lti_message_type parameter.';
|
|
|
684 |
}
|
|
|
685 |
if ($this->ok) {
|
|
|
686 |
$this->ok = isset($_POST['lti_version']) && in_array($_POST['lti_version'], self::$LTI_VERSIONS);
|
|
|
687 |
if (!$this->ok) {
|
|
|
688 |
$this->reason = 'Invalid or missing lti_version parameter.';
|
|
|
689 |
}
|
|
|
690 |
}
|
|
|
691 |
if ($this->ok) {
|
|
|
692 |
if ($_POST['lti_message_type'] === 'basic-lti-launch-request') {
|
|
|
693 |
$this->ok = isset($_POST['resource_link_id']) && (strlen(trim($_POST['resource_link_id'])) > 0);
|
|
|
694 |
if (!$this->ok) {
|
|
|
695 |
$this->reason = 'Missing resource link ID.';
|
|
|
696 |
}
|
|
|
697 |
} else if ($_POST['lti_message_type'] === 'ContentItemSelectionRequest') {
|
|
|
698 |
if (isset($_POST['accept_media_types']) && (strlen(trim($_POST['accept_media_types'])) > 0)) {
|
|
|
699 |
$mediaTypes = array_filter(explode(',', str_replace(' ', '', $_POST['accept_media_types'])), 'strlen');
|
|
|
700 |
$mediaTypes = array_unique($mediaTypes);
|
|
|
701 |
$this->ok = count($mediaTypes) > 0;
|
|
|
702 |
if (!$this->ok) {
|
|
|
703 |
$this->reason = 'No accept_media_types found.';
|
|
|
704 |
} else {
|
|
|
705 |
$this->mediaTypes = $mediaTypes;
|
|
|
706 |
}
|
|
|
707 |
} else {
|
|
|
708 |
$this->ok = false;
|
|
|
709 |
}
|
|
|
710 |
if ($this->ok && isset($_POST['accept_presentation_document_targets']) && (strlen(trim($_POST['accept_presentation_document_targets'])) > 0)) {
|
|
|
711 |
$documentTargets = array_filter(explode(',', str_replace(' ', '', $_POST['accept_presentation_document_targets'])), 'strlen');
|
|
|
712 |
$documentTargets = array_unique($documentTargets);
|
|
|
713 |
$this->ok = count($documentTargets) > 0;
|
|
|
714 |
if (!$this->ok) {
|
|
|
715 |
$this->reason = 'Missing or empty accept_presentation_document_targets parameter.';
|
|
|
716 |
} else {
|
|
|
717 |
foreach ($documentTargets as $documentTarget) {
|
|
|
718 |
$this->ok = $this->checkValue($documentTarget, array('embed', 'frame', 'iframe', 'window', 'popup', 'overlay', 'none'),
|
|
|
719 |
'Invalid value in accept_presentation_document_targets parameter: %s.');
|
|
|
720 |
if (!$this->ok) {
|
|
|
721 |
break;
|
|
|
722 |
}
|
|
|
723 |
}
|
|
|
724 |
if ($this->ok) {
|
|
|
725 |
$this->documentTargets = $documentTargets;
|
|
|
726 |
}
|
|
|
727 |
}
|
|
|
728 |
} else {
|
|
|
729 |
$this->ok = false;
|
|
|
730 |
}
|
|
|
731 |
if ($this->ok) {
|
|
|
732 |
$this->ok = isset($_POST['content_item_return_url']) && (strlen(trim($_POST['content_item_return_url'])) > 0);
|
|
|
733 |
if (!$this->ok) {
|
|
|
734 |
$this->reason = 'Missing content_item_return_url parameter.';
|
|
|
735 |
}
|
|
|
736 |
}
|
|
|
737 |
} else if ($_POST['lti_message_type'] == 'ToolProxyRegistrationRequest') {
|
|
|
738 |
$this->ok = ((isset($_POST['reg_key']) && (strlen(trim($_POST['reg_key'])) > 0)) &&
|
|
|
739 |
(isset($_POST['reg_password']) && (strlen(trim($_POST['reg_password'])) > 0)) &&
|
|
|
740 |
(isset($_POST['tc_profile_url']) && (strlen(trim($_POST['tc_profile_url'])) > 0)) &&
|
|
|
741 |
(isset($_POST['launch_presentation_return_url']) && (strlen(trim($_POST['launch_presentation_return_url'])) > 0)));
|
|
|
742 |
if ($this->debugMode && !$this->ok) {
|
|
|
743 |
$this->reason = 'Missing message parameters.';
|
|
|
744 |
}
|
|
|
745 |
}
|
|
|
746 |
}
|
|
|
747 |
$now = time();
|
|
|
748 |
// Check consumer key
|
|
|
749 |
if ($this->ok && ($_POST['lti_message_type'] != 'ToolProxyRegistrationRequest')) {
|
|
|
750 |
$this->ok = isset($_POST['oauth_consumer_key']);
|
|
|
751 |
if (!$this->ok) {
|
|
|
752 |
$this->reason = 'Missing consumer key.';
|
|
|
753 |
}
|
|
|
754 |
if ($this->ok) {
|
|
|
755 |
$this->consumer = new ToolConsumer($_POST['oauth_consumer_key'], $this->dataConnector);
|
|
|
756 |
$this->ok = !is_null($this->consumer->created);
|
|
|
757 |
if (!$this->ok) {
|
|
|
758 |
$this->reason = 'Invalid consumer key.';
|
|
|
759 |
}
|
|
|
760 |
}
|
|
|
761 |
if ($this->ok) {
|
|
|
762 |
$today = date('Y-m-d', $now);
|
|
|
763 |
if (is_null($this->consumer->lastAccess)) {
|
|
|
764 |
$doSaveConsumer = true;
|
|
|
765 |
} else {
|
|
|
766 |
$last = date('Y-m-d', $this->consumer->lastAccess);
|
|
|
767 |
$doSaveConsumer = $doSaveConsumer || ($last !== $today);
|
|
|
768 |
}
|
|
|
769 |
$this->consumer->last_access = $now;
|
|
|
770 |
try {
|
|
|
771 |
$store = new OAuthDataStore($this);
|
|
|
772 |
$server = new OAuth\OAuthServer($store);
|
|
|
773 |
$method = new OAuth\OAuthSignatureMethod_HMAC_SHA1();
|
|
|
774 |
$server->add_signature_method($method);
|
|
|
775 |
$request = OAuth\OAuthRequest::from_request();
|
|
|
776 |
$res = $server->verify_request($request);
|
|
|
777 |
} catch (\Exception $e) {
|
|
|
778 |
$this->ok = false;
|
|
|
779 |
if (empty($this->reason)) {
|
|
|
780 |
if ($this->debugMode) {
|
|
|
781 |
$consumer = new OAuth\OAuthConsumer($this->consumer->getKey(), $this->consumer->secret);
|
|
|
782 |
$signature = $request->build_signature($method, $consumer, false);
|
|
|
783 |
$this->reason = $e->getMessage();
|
|
|
784 |
if (empty($this->reason)) {
|
|
|
785 |
$this->reason = 'OAuth exception';
|
|
|
786 |
}
|
|
|
787 |
$this->details[] = 'Timestamp: ' . time();
|
|
|
788 |
$this->details[] = "Signature: {$signature}";
|
|
|
789 |
$this->details[] = "Base string: {$request->base_string}]";
|
|
|
790 |
} else {
|
|
|
791 |
$this->reason = 'OAuth signature check failed - perhaps an incorrect secret or timestamp.';
|
|
|
792 |
}
|
|
|
793 |
}
|
|
|
794 |
}
|
|
|
795 |
}
|
|
|
796 |
if ($this->ok) {
|
|
|
797 |
$today = date('Y-m-d', $now);
|
|
|
798 |
if (is_null($this->consumer->lastAccess)) {
|
|
|
799 |
$doSaveConsumer = true;
|
|
|
800 |
} else {
|
|
|
801 |
$last = date('Y-m-d', $this->consumer->lastAccess);
|
|
|
802 |
$doSaveConsumer = $doSaveConsumer || ($last !== $today);
|
|
|
803 |
}
|
|
|
804 |
$this->consumer->last_access = $now;
|
|
|
805 |
if ($this->consumer->protected) {
|
|
|
806 |
if (!is_null($this->consumer->consumerGuid)) {
|
|
|
807 |
$this->ok = empty($_POST['tool_consumer_instance_guid']) ||
|
|
|
808 |
($this->consumer->consumerGuid === $_POST['tool_consumer_instance_guid']);
|
|
|
809 |
if (!$this->ok) {
|
|
|
810 |
$this->reason = 'Request is from an invalid tool consumer.';
|
|
|
811 |
}
|
|
|
812 |
}
|
|
|
813 |
}
|
|
|
814 |
if ($this->ok) {
|
|
|
815 |
$this->ok = $this->consumer->enabled;
|
|
|
816 |
if (!$this->ok) {
|
|
|
817 |
$this->reason = 'Tool consumer has not been enabled by the tool provider.';
|
|
|
818 |
}
|
|
|
819 |
}
|
|
|
820 |
if ($this->ok) {
|
|
|
821 |
$this->ok = is_null($this->consumer->enableFrom) || ($this->consumer->enableFrom <= $now);
|
|
|
822 |
if ($this->ok) {
|
|
|
823 |
$this->ok = is_null($this->consumer->enableUntil) || ($this->consumer->enableUntil > $now);
|
|
|
824 |
if (!$this->ok) {
|
|
|
825 |
$this->reason = 'Tool consumer access has expired.';
|
|
|
826 |
}
|
|
|
827 |
} else {
|
|
|
828 |
$this->reason = 'Tool consumer access is not yet available.';
|
|
|
829 |
}
|
|
|
830 |
}
|
|
|
831 |
}
|
|
|
832 |
|
|
|
833 |
// Validate other message parameter values
|
|
|
834 |
if ($this->ok) {
|
|
|
835 |
if ($_POST['lti_message_type'] === 'ContentItemSelectionRequest') {
|
|
|
836 |
if (isset($_POST['accept_unsigned'])) {
|
|
|
837 |
$this->ok = $this->checkValue($_POST['accept_unsigned'], array('true', 'false'), 'Invalid value for accept_unsigned parameter: %s.');
|
|
|
838 |
}
|
|
|
839 |
if ($this->ok && isset($_POST['accept_multiple'])) {
|
|
|
840 |
$this->ok = $this->checkValue($_POST['accept_multiple'], array('true', 'false'), 'Invalid value for accept_multiple parameter: %s.');
|
|
|
841 |
}
|
|
|
842 |
if ($this->ok && isset($_POST['accept_copy_advice'])) {
|
|
|
843 |
$this->ok = $this->checkValue($_POST['accept_copy_advice'], array('true', 'false'), 'Invalid value for accept_copy_advice parameter: %s.');
|
|
|
844 |
}
|
|
|
845 |
if ($this->ok && isset($_POST['auto_create'])) {
|
|
|
846 |
$this->ok = $this->checkValue($_POST['auto_create'], array('true', 'false'), 'Invalid value for auto_create parameter: %s.');
|
|
|
847 |
}
|
|
|
848 |
if ($this->ok && isset($_POST['can_confirm'])) {
|
|
|
849 |
$this->ok = $this->checkValue($_POST['can_confirm'], array('true', 'false'), 'Invalid value for can_confirm parameter: %s.');
|
|
|
850 |
}
|
|
|
851 |
} else if (isset($_POST['launch_presentation_document_target'])) {
|
|
|
852 |
$this->ok = $this->checkValue($_POST['launch_presentation_document_target'], array('embed', 'frame', 'iframe', 'window', 'popup', 'overlay'),
|
|
|
853 |
'Invalid value for launch_presentation_document_target parameter: %s.');
|
|
|
854 |
}
|
|
|
855 |
}
|
|
|
856 |
}
|
|
|
857 |
|
|
|
858 |
if ($this->ok && ($_POST['lti_message_type'] === 'ToolProxyRegistrationRequest')) {
|
|
|
859 |
$this->ok = $_POST['lti_version'] == self::LTI_VERSION2;
|
|
|
860 |
if (!$this->ok) {
|
|
|
861 |
$this->reason = 'Invalid lti_version parameter';
|
|
|
862 |
}
|
|
|
863 |
if ($this->ok) {
|
|
|
864 |
$http = new HTTPMessage($_POST['tc_profile_url'], 'GET', null, 'Accept: application/vnd.ims.lti.v2.toolconsumerprofile+json');
|
|
|
865 |
$this->ok = $http->send();
|
|
|
866 |
if (!$this->ok) {
|
|
|
867 |
$this->reason = 'Tool consumer profile not accessible.';
|
|
|
868 |
} else {
|
|
|
869 |
$tcProfile = json_decode($http->response);
|
|
|
870 |
$this->ok = !is_null($tcProfile);
|
|
|
871 |
if (!$this->ok) {
|
|
|
872 |
$this->reason = 'Invalid JSON in tool consumer profile.';
|
|
|
873 |
}
|
|
|
874 |
}
|
|
|
875 |
}
|
|
|
876 |
// Check for required capabilities
|
|
|
877 |
if ($this->ok) {
|
|
|
878 |
$this->consumer = new ToolConsumer($_POST['reg_key'], $this->dataConnector);
|
|
|
879 |
$this->consumer->profile = $tcProfile;
|
|
|
880 |
$capabilities = $this->consumer->profile->capability_offered;
|
|
|
881 |
$missing = array();
|
|
|
882 |
foreach ($this->resourceHandlers as $resourceHandler) {
|
|
|
883 |
foreach ($resourceHandler->requiredMessages as $message) {
|
|
|
884 |
if (!in_array($message->type, $capabilities)) {
|
|
|
885 |
$missing[$message->type] = true;
|
|
|
886 |
}
|
|
|
887 |
}
|
|
|
888 |
}
|
|
|
889 |
foreach ($this->constraints as $name => $constraint) {
|
|
|
890 |
if ($constraint['required']) {
|
|
|
891 |
if (!in_array($name, $capabilities) && !in_array($name, array_flip($capabilities))) {
|
|
|
892 |
$missing[$name] = true;
|
|
|
893 |
}
|
|
|
894 |
}
|
|
|
895 |
}
|
|
|
896 |
if (!empty($missing)) {
|
|
|
897 |
ksort($missing);
|
|
|
898 |
$this->reason = 'Required capability not offered - \'' . implode('\', \'', array_keys($missing)) . '\'';
|
|
|
899 |
$this->ok = false;
|
|
|
900 |
}
|
|
|
901 |
}
|
|
|
902 |
// Check for required services
|
|
|
903 |
if ($this->ok) {
|
|
|
904 |
foreach ($this->requiredServices as $service) {
|
|
|
905 |
foreach ($service->formats as $format) {
|
|
|
906 |
if (!$this->findService($format, $service->actions)) {
|
|
|
907 |
if ($this->ok) {
|
|
|
908 |
$this->reason = 'Required service(s) not offered - ';
|
|
|
909 |
$this->ok = false;
|
|
|
910 |
} else {
|
|
|
911 |
$this->reason .= ', ';
|
|
|
912 |
}
|
|
|
913 |
$this->reason .= "'{$format}' [" . implode(', ', $service->actions) . ']';
|
|
|
914 |
}
|
|
|
915 |
}
|
|
|
916 |
}
|
|
|
917 |
}
|
|
|
918 |
if ($this->ok) {
|
|
|
919 |
if ($_POST['lti_message_type'] === 'ToolProxyRegistrationRequest') {
|
|
|
920 |
$this->consumer->profile = $tcProfile;
|
|
|
921 |
$this->consumer->secret = $_POST['reg_password'];
|
|
|
922 |
$this->consumer->ltiVersion = $_POST['lti_version'];
|
|
|
923 |
$this->consumer->name = $tcProfile->product_instance->service_owner->service_owner_name->default_value;
|
|
|
924 |
$this->consumer->consumerName = $this->consumer->name;
|
|
|
925 |
$this->consumer->consumerVersion = "{$tcProfile->product_instance->product_info->product_family->code}-{$tcProfile->product_instance->product_info->product_version}";
|
|
|
926 |
$this->consumer->consumerGuid = $tcProfile->product_instance->guid;
|
|
|
927 |
$this->consumer->enabled = true;
|
|
|
928 |
$this->consumer->protected = true;
|
|
|
929 |
$doSaveConsumer = true;
|
|
|
930 |
}
|
|
|
931 |
}
|
|
|
932 |
} else if ($this->ok && !empty($_POST['custom_tc_profile_url']) && empty($this->consumer->profile)) {
|
|
|
933 |
$http = new HTTPMessage($_POST['custom_tc_profile_url'], 'GET', null, 'Accept: application/vnd.ims.lti.v2.toolconsumerprofile+json');
|
|
|
934 |
if ($http->send()) {
|
|
|
935 |
$tcProfile = json_decode($http->response);
|
|
|
936 |
if (!is_null($tcProfile)) {
|
|
|
937 |
$this->consumer->profile = $tcProfile;
|
|
|
938 |
$doSaveConsumer = true;
|
|
|
939 |
}
|
|
|
940 |
}
|
|
|
941 |
}
|
|
|
942 |
|
|
|
943 |
// Validate message parameter constraints
|
|
|
944 |
if ($this->ok) {
|
|
|
945 |
$invalidParameters = array();
|
|
|
946 |
foreach ($this->constraints as $name => $constraint) {
|
|
|
947 |
if (empty($constraint['messages']) || in_array($_POST['lti_message_type'], $constraint['messages'])) {
|
|
|
948 |
$ok = true;
|
|
|
949 |
if ($constraint['required']) {
|
|
|
950 |
if (!isset($_POST[$name]) || (strlen(trim($_POST[$name])) <= 0)) {
|
|
|
951 |
$invalidParameters[] = "{$name} (missing)";
|
|
|
952 |
$ok = false;
|
|
|
953 |
}
|
|
|
954 |
}
|
|
|
955 |
if ($ok && !is_null($constraint['max_length']) && isset($_POST[$name])) {
|
|
|
956 |
if (strlen(trim($_POST[$name])) > $constraint['max_length']) {
|
|
|
957 |
$invalidParameters[] = "{$name} (too long)";
|
|
|
958 |
}
|
|
|
959 |
}
|
|
|
960 |
}
|
|
|
961 |
}
|
|
|
962 |
if (count($invalidParameters) > 0) {
|
|
|
963 |
$this->ok = false;
|
|
|
964 |
if (empty($this->reason)) {
|
|
|
965 |
$this->reason = 'Invalid parameter(s): ' . implode(', ', $invalidParameters) . '.';
|
|
|
966 |
}
|
|
|
967 |
}
|
|
|
968 |
}
|
|
|
969 |
|
|
|
970 |
if ($this->ok) {
|
|
|
971 |
|
|
|
972 |
// Set the request context
|
|
|
973 |
if (isset($_POST['context_id'])) {
|
|
|
974 |
$this->context = Context::fromConsumer($this->consumer, trim($_POST['context_id']));
|
|
|
975 |
$title = '';
|
|
|
976 |
if (isset($_POST['context_title'])) {
|
|
|
977 |
$title = trim($_POST['context_title']);
|
|
|
978 |
}
|
|
|
979 |
if (empty($title)) {
|
|
|
980 |
$title = "Course {$this->context->getId()}";
|
|
|
981 |
}
|
|
|
982 |
if (isset($_POST['context_type'])) {
|
|
|
983 |
$this->context->type = trim($_POST['context_type']);
|
|
|
984 |
}
|
|
|
985 |
$this->context->title = $title;
|
|
|
986 |
}
|
|
|
987 |
|
|
|
988 |
// Set the request resource link
|
|
|
989 |
if (isset($_POST['resource_link_id'])) {
|
|
|
990 |
$contentItemId = '';
|
|
|
991 |
if (isset($_POST['custom_content_item_id'])) {
|
|
|
992 |
$contentItemId = $_POST['custom_content_item_id'];
|
|
|
993 |
}
|
|
|
994 |
$this->resourceLink = ResourceLink::fromConsumer($this->consumer, trim($_POST['resource_link_id']), $contentItemId);
|
|
|
995 |
if (!empty($this->context)) {
|
|
|
996 |
$this->resourceLink->setContextId($this->context->getRecordId());
|
|
|
997 |
}
|
|
|
998 |
$title = '';
|
|
|
999 |
if (isset($_POST['resource_link_title'])) {
|
|
|
1000 |
$title = trim($_POST['resource_link_title']);
|
|
|
1001 |
}
|
|
|
1002 |
if (empty($title)) {
|
|
|
1003 |
$title = "Resource {$this->resourceLink->getId()}";
|
|
|
1004 |
}
|
|
|
1005 |
$this->resourceLink->title = $title;
|
|
|
1006 |
// Delete any existing custom parameters
|
|
|
1007 |
foreach ($this->consumer->getSettings() as $name => $value) {
|
|
|
1008 |
if (strpos($name, 'custom_') === 0) {
|
|
|
1009 |
$this->consumer->setSetting($name);
|
|
|
1010 |
$doSaveConsumer = true;
|
|
|
1011 |
}
|
|
|
1012 |
}
|
|
|
1013 |
if (!empty($this->context)) {
|
|
|
1014 |
foreach ($this->context->getSettings() as $name => $value) {
|
|
|
1015 |
if (strpos($name, 'custom_') === 0) {
|
|
|
1016 |
$this->context->setSetting($name);
|
|
|
1017 |
}
|
|
|
1018 |
}
|
|
|
1019 |
}
|
|
|
1020 |
foreach ($this->resourceLink->getSettings() as $name => $value) {
|
|
|
1021 |
if (strpos($name, 'custom_') === 0) {
|
|
|
1022 |
$this->resourceLink->setSetting($name);
|
|
|
1023 |
}
|
|
|
1024 |
}
|
|
|
1025 |
// Save LTI parameters
|
|
|
1026 |
foreach (self::$LTI_CONSUMER_SETTING_NAMES as $name) {
|
|
|
1027 |
if (isset($_POST[$name])) {
|
|
|
1028 |
$this->consumer->setSetting($name, $_POST[$name]);
|
|
|
1029 |
} else {
|
|
|
1030 |
$this->consumer->setSetting($name);
|
|
|
1031 |
}
|
|
|
1032 |
}
|
|
|
1033 |
if (!empty($this->context)) {
|
|
|
1034 |
foreach (self::$LTI_CONTEXT_SETTING_NAMES as $name) {
|
|
|
1035 |
if (isset($_POST[$name])) {
|
|
|
1036 |
$this->context->setSetting($name, $_POST[$name]);
|
|
|
1037 |
} else {
|
|
|
1038 |
$this->context->setSetting($name);
|
|
|
1039 |
}
|
|
|
1040 |
}
|
|
|
1041 |
}
|
|
|
1042 |
foreach (self::$LTI_RESOURCE_LINK_SETTING_NAMES as $name) {
|
|
|
1043 |
if (isset($_POST[$name])) {
|
|
|
1044 |
$this->resourceLink->setSetting($name, $_POST[$name]);
|
|
|
1045 |
} else {
|
|
|
1046 |
$this->resourceLink->setSetting($name);
|
|
|
1047 |
}
|
|
|
1048 |
}
|
|
|
1049 |
// Save other custom parameters
|
|
|
1050 |
foreach ($_POST as $name => $value) {
|
|
|
1051 |
if ((strpos($name, 'custom_') === 0) &&
|
|
|
1052 |
!in_array($name, array_merge(self::$LTI_CONSUMER_SETTING_NAMES, self::$LTI_CONTEXT_SETTING_NAMES, self::$LTI_RESOURCE_LINK_SETTING_NAMES))) {
|
|
|
1053 |
$this->resourceLink->setSetting($name, $value);
|
|
|
1054 |
}
|
|
|
1055 |
}
|
|
|
1056 |
}
|
|
|
1057 |
|
|
|
1058 |
// Set the user instance
|
|
|
1059 |
$userId = '';
|
|
|
1060 |
if (isset($_POST['user_id'])) {
|
|
|
1061 |
$userId = trim($_POST['user_id']);
|
|
|
1062 |
}
|
|
|
1063 |
|
|
|
1064 |
$this->user = User::fromResourceLink($this->resourceLink, $userId);
|
|
|
1065 |
|
|
|
1066 |
// Set the user name
|
|
|
1067 |
$firstname = (isset($_POST['lis_person_name_given'])) ? $_POST['lis_person_name_given'] : '';
|
|
|
1068 |
$lastname = (isset($_POST['lis_person_name_family'])) ? $_POST['lis_person_name_family'] : '';
|
|
|
1069 |
$fullname = (isset($_POST['lis_person_name_full'])) ? $_POST['lis_person_name_full'] : '';
|
|
|
1070 |
$this->user->setNames($firstname, $lastname, $fullname);
|
|
|
1071 |
|
|
|
1072 |
// Set the user email
|
|
|
1073 |
$email = (isset($_POST['lis_person_contact_email_primary'])) ? $_POST['lis_person_contact_email_primary'] : '';
|
|
|
1074 |
$this->user->setEmail($email, $this->defaultEmail);
|
|
|
1075 |
|
|
|
1076 |
// Set the user image URI
|
|
|
1077 |
if (isset($_POST['user_image'])) {
|
|
|
1078 |
$this->user->image = $_POST['user_image'];
|
|
|
1079 |
}
|
|
|
1080 |
|
|
|
1081 |
// Set the user roles
|
|
|
1082 |
if (isset($_POST['roles'])) {
|
|
|
1083 |
$this->user->roles = self::parseRoles($_POST['roles']);
|
|
|
1084 |
}
|
|
|
1085 |
|
|
|
1086 |
// Initialise the consumer and check for changes
|
|
|
1087 |
$this->consumer->defaultEmail = $this->defaultEmail;
|
|
|
1088 |
if ($this->consumer->ltiVersion !== $_POST['lti_version']) {
|
|
|
1089 |
$this->consumer->ltiVersion = $_POST['lti_version'];
|
|
|
1090 |
$doSaveConsumer = true;
|
|
|
1091 |
}
|
|
|
1092 |
if (isset($_POST['tool_consumer_instance_name'])) {
|
|
|
1093 |
if ($this->consumer->consumerName !== $_POST['tool_consumer_instance_name']) {
|
|
|
1094 |
$this->consumer->consumerName = $_POST['tool_consumer_instance_name'];
|
|
|
1095 |
$doSaveConsumer = true;
|
|
|
1096 |
}
|
|
|
1097 |
}
|
|
|
1098 |
if (isset($_POST['tool_consumer_info_product_family_code'])) {
|
|
|
1099 |
$version = $_POST['tool_consumer_info_product_family_code'];
|
|
|
1100 |
if (isset($_POST['tool_consumer_info_version'])) {
|
|
|
1101 |
$version .= "-{$_POST['tool_consumer_info_version']}";
|
|
|
1102 |
}
|
|
|
1103 |
// do not delete any existing consumer version if none is passed
|
|
|
1104 |
if ($this->consumer->consumerVersion !== $version) {
|
|
|
1105 |
$this->consumer->consumerVersion = $version;
|
|
|
1106 |
$doSaveConsumer = true;
|
|
|
1107 |
}
|
|
|
1108 |
} else if (isset($_POST['ext_lms']) && ($this->consumer->consumerName !== $_POST['ext_lms'])) {
|
|
|
1109 |
$this->consumer->consumerVersion = $_POST['ext_lms'];
|
|
|
1110 |
$doSaveConsumer = true;
|
|
|
1111 |
}
|
|
|
1112 |
if (isset($_POST['tool_consumer_instance_guid'])) {
|
|
|
1113 |
if (is_null($this->consumer->consumerGuid)) {
|
|
|
1114 |
$this->consumer->consumerGuid = $_POST['tool_consumer_instance_guid'];
|
|
|
1115 |
$doSaveConsumer = true;
|
|
|
1116 |
} else if (!$this->consumer->protected) {
|
|
|
1117 |
$doSaveConsumer = ($this->consumer->consumerGuid !== $_POST['tool_consumer_instance_guid']);
|
|
|
1118 |
if ($doSaveConsumer) {
|
|
|
1119 |
$this->consumer->consumerGuid = $_POST['tool_consumer_instance_guid'];
|
|
|
1120 |
}
|
|
|
1121 |
}
|
|
|
1122 |
}
|
|
|
1123 |
if (isset($_POST['launch_presentation_css_url'])) {
|
|
|
1124 |
if ($this->consumer->cssPath !== $_POST['launch_presentation_css_url']) {
|
|
|
1125 |
$this->consumer->cssPath = $_POST['launch_presentation_css_url'];
|
|
|
1126 |
$doSaveConsumer = true;
|
|
|
1127 |
}
|
|
|
1128 |
} else if (isset($_POST['ext_launch_presentation_css_url']) &&
|
|
|
1129 |
($this->consumer->cssPath !== $_POST['ext_launch_presentation_css_url'])) {
|
|
|
1130 |
$this->consumer->cssPath = $_POST['ext_launch_presentation_css_url'];
|
|
|
1131 |
$doSaveConsumer = true;
|
|
|
1132 |
} else if (!empty($this->consumer->cssPath)) {
|
|
|
1133 |
$this->consumer->cssPath = null;
|
|
|
1134 |
$doSaveConsumer = true;
|
|
|
1135 |
}
|
|
|
1136 |
}
|
|
|
1137 |
|
|
|
1138 |
// Persist changes to consumer
|
|
|
1139 |
if ($doSaveConsumer) {
|
|
|
1140 |
$this->consumer->save();
|
|
|
1141 |
}
|
|
|
1142 |
if ($this->ok && isset($this->context)) {
|
|
|
1143 |
$this->context->save();
|
|
|
1144 |
}
|
|
|
1145 |
if ($this->ok && isset($this->resourceLink)) {
|
|
|
1146 |
|
|
|
1147 |
// Check if a share arrangement is in place for this resource link
|
|
|
1148 |
$this->ok = $this->checkForShare();
|
|
|
1149 |
|
|
|
1150 |
// Persist changes to resource link
|
|
|
1151 |
$this->resourceLink->save();
|
|
|
1152 |
|
|
|
1153 |
// Save the user instance
|
|
|
1154 |
if (isset($_POST['lis_result_sourcedid'])) {
|
|
|
1155 |
if ($this->user->ltiResultSourcedId !== $_POST['lis_result_sourcedid']) {
|
|
|
1156 |
$this->user->ltiResultSourcedId = $_POST['lis_result_sourcedid'];
|
|
|
1157 |
$this->user->save();
|
|
|
1158 |
}
|
|
|
1159 |
} else if (!empty($this->user->ltiResultSourcedId)) {
|
|
|
1160 |
$this->user->ltiResultSourcedId = '';
|
|
|
1161 |
$this->user->save();
|
|
|
1162 |
}
|
|
|
1163 |
}
|
|
|
1164 |
|
|
|
1165 |
return $this->ok;
|
|
|
1166 |
|
|
|
1167 |
}
|
|
|
1168 |
|
|
|
1169 |
/**
|
|
|
1170 |
* Check if a share arrangement is in place.
|
|
|
1171 |
*
|
|
|
1172 |
* @return boolean True if no error is reported
|
|
|
1173 |
*/
|
|
|
1174 |
private function checkForShare()
|
|
|
1175 |
{
|
|
|
1176 |
|
|
|
1177 |
$ok = true;
|
|
|
1178 |
$doSaveResourceLink = true;
|
|
|
1179 |
|
|
|
1180 |
$id = $this->resourceLink->primaryResourceLinkId;
|
|
|
1181 |
|
|
|
1182 |
$shareRequest = isset($_POST['custom_share_key']) && !empty($_POST['custom_share_key']);
|
|
|
1183 |
if ($shareRequest) {
|
|
|
1184 |
if (!$this->allowSharing) {
|
|
|
1185 |
$ok = false;
|
|
|
1186 |
$this->reason = 'Your sharing request has been refused because sharing is not being permitted.';
|
|
|
1187 |
} else {
|
|
|
1188 |
// Check if this is a new share key
|
|
|
1189 |
$shareKey = new ResourceLinkShareKey($this->resourceLink, $_POST['custom_share_key']);
|
|
|
1190 |
if (!is_null($shareKey->primaryConsumerKey) && !is_null($shareKey->primaryResourceLinkId)) {
|
|
|
1191 |
// Update resource link with sharing primary resource link details
|
|
|
1192 |
$key = $shareKey->primaryConsumerKey;
|
|
|
1193 |
$id = $shareKey->primaryResourceLinkId;
|
|
|
1194 |
$ok = ($key !== $this->consumer->getKey()) || ($id != $this->resourceLink->getId());
|
|
|
1195 |
if ($ok) {
|
|
|
1196 |
$this->resourceLink->primaryConsumerKey = $key;
|
|
|
1197 |
$this->resourceLink->primaryResourceLinkId = $id;
|
|
|
1198 |
$this->resourceLink->shareApproved = $shareKey->autoApprove;
|
|
|
1199 |
$ok = $this->resourceLink->save();
|
|
|
1200 |
if ($ok) {
|
|
|
1201 |
$doSaveResourceLink = false;
|
|
|
1202 |
$this->user->getResourceLink()->primaryConsumerKey = $key;
|
|
|
1203 |
$this->user->getResourceLink()->primaryResourceLinkId = $id;
|
|
|
1204 |
$this->user->getResourceLink()->shareApproved = $shareKey->autoApprove;
|
|
|
1205 |
$this->user->getResourceLink()->updated = time();
|
|
|
1206 |
// Remove share key
|
|
|
1207 |
$shareKey->delete();
|
|
|
1208 |
} else {
|
|
|
1209 |
$this->reason = 'An error occurred initialising your share arrangement.';
|
|
|
1210 |
}
|
|
|
1211 |
} else {
|
|
|
1212 |
$this->reason = 'It is not possible to share your resource link with yourself.';
|
|
|
1213 |
}
|
|
|
1214 |
}
|
|
|
1215 |
if ($ok) {
|
|
|
1216 |
$ok = !is_null($key);
|
|
|
1217 |
if (!$ok) {
|
|
|
1218 |
$this->reason = 'You have requested to share a resource link but none is available.';
|
|
|
1219 |
} else {
|
|
|
1220 |
$ok = (!is_null($this->user->getResourceLink()->shareApproved) && $this->user->getResourceLink()->shareApproved);
|
|
|
1221 |
if (!$ok) {
|
|
|
1222 |
$this->reason = 'Your share request is waiting to be approved.';
|
|
|
1223 |
}
|
|
|
1224 |
}
|
|
|
1225 |
}
|
|
|
1226 |
}
|
|
|
1227 |
} else {
|
|
|
1228 |
// Check no share is in place
|
|
|
1229 |
$ok = is_null($id);
|
|
|
1230 |
if (!$ok) {
|
|
|
1231 |
$this->reason = 'You have not requested to share a resource link but an arrangement is currently in place.';
|
|
|
1232 |
}
|
|
|
1233 |
}
|
|
|
1234 |
|
|
|
1235 |
// Look up primary resource link
|
|
|
1236 |
if ($ok && !is_null($id)) {
|
|
|
1237 |
$consumer = new ToolConsumer($key, $this->dataConnector);
|
|
|
1238 |
$ok = !is_null($consumer->created);
|
|
|
1239 |
if ($ok) {
|
|
|
1240 |
$resourceLink = ResourceLink::fromConsumer($consumer, $id);
|
|
|
1241 |
$ok = !is_null($resourceLink->created);
|
|
|
1242 |
}
|
|
|
1243 |
if ($ok) {
|
|
|
1244 |
if ($doSaveResourceLink) {
|
|
|
1245 |
$this->resourceLink->save();
|
|
|
1246 |
}
|
|
|
1247 |
$this->resourceLink = $resourceLink;
|
|
|
1248 |
} else {
|
|
|
1249 |
$this->reason = 'Unable to load resource link being shared.';
|
|
|
1250 |
}
|
|
|
1251 |
}
|
|
|
1252 |
|
|
|
1253 |
return $ok;
|
|
|
1254 |
|
|
|
1255 |
}
|
|
|
1256 |
|
|
|
1257 |
/**
|
|
|
1258 |
* Validate a parameter value from an array of permitted values.
|
|
|
1259 |
*
|
|
|
1260 |
* @return boolean True if value is valid
|
|
|
1261 |
*/
|
|
|
1262 |
private function checkValue($value, $values, $reason)
|
|
|
1263 |
{
|
|
|
1264 |
|
|
|
1265 |
$ok = in_array($value, $values);
|
|
|
1266 |
if (!$ok && !empty($reason)) {
|
|
|
1267 |
$this->reason = sprintf($reason, $value);
|
|
|
1268 |
}
|
|
|
1269 |
|
|
|
1270 |
return $ok;
|
|
|
1271 |
|
|
|
1272 |
}
|
|
|
1273 |
|
|
|
1274 |
}
|