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