Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
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 .= '&lti_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
}