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 DOMDocument;
6
use DOMElement;
7
use IMSGlobal\LTI\ToolProvider\DataConnector\DataConnector;
8
use IMSGlobal\LTI\ToolProvider\Service;
9
use IMSGlobal\LTI\HTTPMessage;
10
use IMSGlobal\LTI\OAuth;
11
 
12
/**
13
 * Class to represent a tool consumer resource link
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 http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0
20
 */
21
#[\AllowDynamicProperties]
22
class ResourceLink
23
{
24
 
25
/**
26
 * Read action.
27
 */
28
    const EXT_READ = 1;
29
/**
30
 * Write (create/update) action.
31
 */
32
    const EXT_WRITE = 2;
33
/**
34
 * Delete action.
35
 */
36
    const EXT_DELETE = 3;
37
/**
38
 * Create action.
39
 */
40
    const EXT_CREATE = 4;
41
/**
42
 * Update action.
43
 */
44
    const EXT_UPDATE = 5;
45
 
46
/**
47
 * Decimal outcome type.
48
 */
49
    const EXT_TYPE_DECIMAL = 'decimal';
50
/**
51
 * Percentage outcome type.
52
 */
53
    const EXT_TYPE_PERCENTAGE = 'percentage';
54
/**
55
 * Ratio outcome type.
56
 */
57
    const EXT_TYPE_RATIO = 'ratio';
58
/**
59
 * Letter (A-F) outcome type.
60
 */
61
    const EXT_TYPE_LETTER_AF = 'letteraf';
62
/**
63
 * Letter (A-F) with optional +/- outcome type.
64
 */
65
    const EXT_TYPE_LETTER_AF_PLUS = 'letterafplus';
66
/**
67
 * Pass/fail outcome type.
68
 */
69
    const EXT_TYPE_PASS_FAIL = 'passfail';
70
/**
71
 * Free text outcome type.
72
 */
73
    const EXT_TYPE_TEXT = 'freetext';
74
 
75
/**
76
 * Context title.
77
 *
78
 * @var string $title
79
 */
80
    public $title = null;
81
/**
82
 * Resource link ID as supplied in the last connection request.
83
 *
84
 * @var string $ltiResourceLinkId
85
 */
86
    public $ltiResourceLinkId = null;
87
/**
88
 * User group sets (null if the consumer does not support the groups enhancement)
89
 *
90
 * @var array $groupSets
91
 */
92
    public $groupSets = null;
93
/**
94
 * User groups (null if the consumer does not support the groups enhancement)
95
 *
96
 * @var array $groups
97
 */
98
    public $groups = null;
99
/**
100
 * Request for last service request.
101
 *
102
 * @var string $extRequest
103
 */
104
    public $extRequest = null;
105
/**
106
 * Request headers for last service request.
107
 *
108
 * @var array $extRequestHeaders
109
 */
110
    public $extRequestHeaders = null;
111
/**
112
 * Response from last service request.
113
 *
114
 * @var string $extResponse
115
 */
116
    public $extResponse = null;
117
/**
118
 * Response header from last service request.
119
 *
120
 * @var array $extResponseHeaders
121
 */
122
    public $extResponseHeaders = null;
123
/**
124
 * Consumer key value for resource link being shared (if any).
125
 *
126
 * @var string $primaryResourceLinkId
127
 */
128
    public $primaryResourceLinkId = null;
129
/**
130
 * Whether the sharing request has been approved by the primary resource link.
131
 *
132
 * @var boolean $shareApproved
133
 */
134
    public $shareApproved = null;
135
/**
136
 * Date/time when the object was created.
137
 *
138
 * @var int $created
139
 */
140
    public $created = null;
141
/**
142
 * Date/time when the object was last updated.
143
 *
144
 * @var int $updated
145
 */
146
    public $updated = null;
147
 
148
/**
149
 * Record ID for this resource link.
150
 *
151
 * @var int $id
152
 */
153
    private $id = null;
154
/**
155
 * Tool Consumer for this resource link.
156
 *
157
 * @var ToolConsumer $consumer
158
 */
159
    private $consumer = null;
160
/**
161
 * Tool Consumer ID for this resource link.
162
 *
163
 * @var int $consumerId
164
 */
165
    private $consumerId = null;
166
/**
167
 * Context for this resource link.
168
 *
169
 * @var Context $context
170
 */
171
    private $context = null;
172
/**
173
 * Context ID for this resource link.
174
 *
175
 * @var int $contextId
176
 */
177
    private $contextId = null;
178
/**
179
 * Setting values (LTI parameters, custom parameters and local parameters).
180
 *
181
 * @var array $settings
182
 */
183
    private $settings = null;
184
/**
185
 * Whether the settings value have changed since last saved.
186
 *
187
 * @var boolean $settingsChanged
188
 */
189
    private $settingsChanged = false;
190
/**
191
 * XML document for the last extension service request.
192
 *
193
 * @var string $extDoc
194
 */
195
    private $extDoc = null;
196
/**
197
 * XML node array for the last extension service request.
198
 *
199
 * @var array $extNodes
200
 */
201
    private $extNodes = null;
202
/**
203
 * Data connector object or string.
204
 *
205
 * @var mixed $dataConnector
206
 */
207
    private $dataConnector = null;
208
 
209
/**
210
 * Class constructor.
211
 */
212
    public function __construct()
213
    {
214
 
215
        $this->initialize();
216
 
217
    }
218
 
219
/**
220
 * Initialise the resource link.
221
 */
222
    public function initialize()
223
    {
224
 
225
        $this->title = '';
226
        $this->settings = array();
227
        $this->groupSets = null;
228
        $this->groups = null;
229
        $this->primaryResourceLinkId = null;
230
        $this->shareApproved = null;
231
        $this->created = null;
232
        $this->updated = null;
233
 
234
    }
235
 
236
/**
237
 * Initialise the resource link.
238
 *
239
 * Pseudonym for initialize().
240
 */
241
    public function initialise()
242
    {
243
 
244
        $this->initialize();
245
 
246
    }
247
 
248
/**
249
 * Save the resource link to the database.
250
 *
251
 * @return boolean True if the resource link was successfully saved.
252
 */
253
    public function save()
254
    {
255
 
256
        $ok = $this->getDataConnector()->saveResourceLink($this);
257
        if ($ok) {
258
            $this->settingsChanged = false;
259
        }
260
 
261
        return $ok;
262
 
263
    }
264
 
265
/**
266
 * Delete the resource link from the database.
267
 *
268
 * @return boolean True if the resource link was successfully deleted.
269
 */
270
    public function delete()
271
    {
272
 
273
        return $this->getDataConnector()->deleteResourceLink($this);
274
 
275
    }
276
 
277
/**
278
 * Get tool consumer.
279
 *
280
 * @return ToolConsumer Tool consumer object for this resource link.
281
 */
282
    public function getConsumer()
283
    {
284
 
285
        if (is_null($this->consumer)) {
286
            if (!is_null($this->context) || !is_null($this->contextId)) {
287
                $this->consumer = $this->getContext()->getConsumer();
288
            } else {
289
                $this->consumer = ToolConsumer::fromRecordId($this->consumerId, $this->getDataConnector());
290
            }
291
        }
292
 
293
        return $this->consumer;
294
 
295
    }
296
 
297
/**
298
 * Set tool consumer ID.
299
 *
300
 * @param int $consumerId   Tool Consumer ID for this resource link.
301
 */
302
    public function setConsumerId($consumerId)
303
    {
304
 
305
        $this->consumer = null;
306
        $this->consumerId = $consumerId;
307
 
308
    }
309
 
310
/**
311
 * Get context.
312
 *
313
 * @return object LTIContext object for this resource link.
314
 */
315
    public function getContext()
316
    {
317
 
318
        if (is_null($this->context) && !is_null($this->contextId)) {
319
            $this->context = Context::fromRecordId($this->contextId, $this->getDataConnector());
320
        }
321
 
322
        return $this->context;
323
 
324
    }
325
 
326
/**
327
 * Get context record ID.
328
 *
329
 * @return int Context record ID for this resource link.
330
 */
331
    public function getContextId()
332
    {
333
 
334
        return $this->contextId;
335
 
336
    }
337
 
338
/**
339
 * Set context ID.
340
 *
341
 * @param int $contextId   Context ID for this resource link.
342
 */
343
    public function setContextId($contextId)
344
    {
345
 
346
        $this->context = null;
347
        $this->contextId = $contextId;
348
 
349
    }
350
 
351
/**
352
 * Get tool consumer key.
353
 *
354
 * @return string Consumer key value for this resource link.
355
 */
356
    public function getKey()
357
    {
358
 
359
        return $this->getConsumer()->getKey();
360
 
361
    }
362
 
363
/**
364
 * Get resource link ID.
365
 *
366
 * @return string ID for this resource link.
367
 */
368
    public function getId()
369
    {
370
 
371
        return $this->ltiResourceLinkId;
372
 
373
    }
374
 
375
/**
376
 * Get resource link record ID.
377
 *
378
 * @return int Record ID for this resource link.
379
 */
380
    public function getRecordId()
381
    {
382
 
383
        return $this->id;
384
 
385
    }
386
 
387
/**
388
 * Set resource link record ID.
389
 *
390
 * @param int $id  Record ID for this resource link.
391
 */
392
    public function setRecordId($id)
393
    {
394
 
395
        $this->id = $id;
396
 
397
  }
398
 
399
/**
400
 * Get the data connector.
401
 *
402
 * @return mixed Data connector object or string
403
 */
404
    public function getDataConnector()
405
    {
406
 
407
        return $this->dataConnector;
408
 
409
    }
410
 
411
/**
412
 * Get a setting value.
413
 *
414
 * @param string $name    Name of setting
415
 * @param string $default Value to return if the setting does not exist (optional, default is an empty string)
416
 *
417
 * @return string Setting value
418
 */
419
    public function getSetting($name, $default = '')
420
    {
421
 
422
        if (array_key_exists($name, $this->settings)) {
423
            $value = $this->settings[$name];
424
        } else {
425
            $value = $default;
426
        }
427
 
428
        return $value;
429
 
430
    }
431
 
432
/**
433
 * Set a setting value.
434
 *
435
 * @param string $name  Name of setting
436
 * @param string $value Value to set, use an empty value to delete a setting (optional, default is null)
437
 */
438
    public function setSetting($name, $value = null)
439
    {
440
 
441
        $old_value = $this->getSetting($name);
442
        if ($value !== $old_value) {
443
            if (!empty($value)) {
444
                $this->settings[$name] = $value;
445
            } else {
446
                unset($this->settings[$name]);
447
            }
448
            $this->settingsChanged = true;
449
        }
450
 
451
    }
452
 
453
/**
454
 * Get an array of all setting values.
455
 *
456
 * @return array Associative array of setting values
457
 */
458
    public function getSettings()
459
    {
460
 
461
        return $this->settings;
462
 
463
    }
464
 
465
/**
466
 * Set an array of all setting values.
467
 *
468
 * @param array $settings  Associative array of setting values
469
 */
470
    public function setSettings($settings)
471
    {
472
 
473
        $this->settings = $settings;
474
 
475
    }
476
 
477
/**
478
 * Save setting values.
479
 *
480
 * @return boolean True if the settings were successfully saved
481
 */
482
    public function saveSettings()
483
    {
484
 
485
        if ($this->settingsChanged) {
486
            $ok = $this->save();
487
        } else {
488
            $ok = true;
489
        }
490
 
491
        return $ok;
492
 
493
    }
494
 
495
/**
496
 * Check if the Outcomes service is supported.
497
 *
498
 * @return boolean True if this resource link supports the Outcomes service (either the LTI 1.1 or extension service)
499
 */
500
    public function hasOutcomesService()
501
    {
502
 
503
        $url = $this->getSetting('ext_ims_lis_basic_outcome_url') . $this->getSetting('lis_outcome_service_url');
504
 
505
        return !empty($url);
506
 
507
    }
508
 
509
/**
510
 * Check if the Memberships extension service is supported.
511
 *
512
 * @return boolean True if this resource link supports the Memberships extension service
513
 */
514
    public function hasMembershipsService()
515
    {
516
 
517
        $url = $this->getSetting('ext_ims_lis_memberships_url');
518
 
519
        return !empty($url);
520
 
521
    }
522
 
523
/**
524
 * Check if the Setting extension service is supported.
525
 *
526
 * @return boolean True if this resource link supports the Setting extension service
527
 */
528
    public function hasSettingService()
529
    {
530
 
531
        $url = $this->getSetting('ext_ims_lti_tool_setting_url');
532
 
533
        return !empty($url);
534
 
535
    }
536
 
537
/**
538
 * Perform an Outcomes service request.
539
 *
540
 * @param int $action The action type constant
541
 * @param Outcome $ltiOutcome Outcome object
542
 * @param User $user User object
543
 *
544
 * @return boolean True if the request was successfully processed
545
 */
546
    public function doOutcomesService($action, $ltiOutcome, $user)
547
    {
548
 
549
        $response = false;
550
        $this->extResponse = null;
551
 
552
// Lookup service details from the source resource link appropriate to the user (in case the destination is being shared)
553
        $sourceResourceLink = $user->getResourceLink();
554
        $sourcedId = $user->ltiResultSourcedId;
555
 
556
// Use LTI 1.1 service in preference to extension service if it is available
557
        $urlLTI11 = $sourceResourceLink->getSetting('lis_outcome_service_url');
558
        $urlExt = $sourceResourceLink->getSetting('ext_ims_lis_basic_outcome_url');
559
        if ($urlExt || $urlLTI11) {
560
            switch ($action) {
561
                case self::EXT_READ:
562
                    if ($urlLTI11 && ($ltiOutcome->type === self::EXT_TYPE_DECIMAL)) {
563
                        $do = 'readResult';
564
                    } else if ($urlExt) {
565
                        $urlLTI11 = null;
566
                        $do = 'basic-lis-readresult';
567
                    }
568
                    break;
569
                case self::EXT_WRITE:
570
                    if ($urlLTI11 && $this->checkValueType($ltiOutcome, array(self::EXT_TYPE_DECIMAL))) {
571
                        $do = 'replaceResult';
572
                    } else if ($this->checkValueType($ltiOutcome)) {
573
                        $urlLTI11 = null;
574
                        $do = 'basic-lis-updateresult';
575
                    }
576
                    break;
577
                case self::EXT_DELETE:
578
                    if ($urlLTI11 && ($ltiOutcome->type === self::EXT_TYPE_DECIMAL)) {
579
                        $do = 'deleteResult';
580
                    } else if ($urlExt) {
581
                        $urlLTI11 = null;
582
                        $do = 'basic-lis-deleteresult';
583
                    }
584
                    break;
585
            }
586
        }
587
        if (isset($do)) {
588
            $value = $ltiOutcome->getValue();
589
            if (is_null($value)) {
590
                $value = '';
591
            }
592
            if ($urlLTI11) {
593
                $xml = '';
594
                if ($action === self::EXT_WRITE) {
595
                    $xml = <<<EOF
596
 
597
        <result>
598
          <resultScore>
599
            <language>{$ltiOutcome->language}</language>
600
            <textString>{$value}</textString>
601
          </resultScore>
602
        </result>
603
EOF;
604
                }
605
                $sourcedId = htmlentities($sourcedId);
606
                $xml = <<<EOF
607
      <resultRecord>
608
        <sourcedGUID>
609
          <sourcedId>{$sourcedId}</sourcedId>
610
        </sourcedGUID>{$xml}
611
      </resultRecord>
612
EOF;
613
                if ($this->doLTI11Service($do, $urlLTI11, $xml)) {
614
                    switch ($action) {
615
                        case self::EXT_READ:
616
                            if (!isset($this->extNodes['imsx_POXBody']["{$do}Response"]['result']['resultScore']['textString'])) {
617
                                break;
618
                            } else {
619
                                $ltiOutcome->setValue($this->extNodes['imsx_POXBody']["{$do}Response"]['result']['resultScore']['textString']);
620
                            }
621
                        case self::EXT_WRITE:
622
                        case self::EXT_DELETE:
623
                            $response = true;
624
                            break;
625
                    }
626
                }
627
            } else {
628
                $params = array();
629
                $params['sourcedid'] = $sourcedId;
630
                $params['result_resultscore_textstring'] = $value;
631
                if (!empty($ltiOutcome->language)) {
632
                    $params['result_resultscore_language'] = $ltiOutcome->language;
633
                }
634
                if (!empty($ltiOutcome->status)) {
635
                    $params['result_statusofresult'] = $ltiOutcome->status;
636
                }
637
                if (!empty($ltiOutcome->date)) {
638
                    $params['result_date'] = $ltiOutcome->date;
639
                }
640
                if (!empty($ltiOutcome->type)) {
641
                    $params['result_resultvaluesourcedid'] = $ltiOutcome->type;
642
                }
643
                if (!empty($ltiOutcome->data_source)) {
644
                    $params['result_datasource'] = $ltiOutcome->data_source;
645
                }
646
                if ($this->doService($do, $urlExt, $params)) {
647
                    switch ($action) {
648
                        case self::EXT_READ:
649
                            if (isset($this->extNodes['result']['resultscore']['textstring'])) {
650
                                $response = $this->extNodes['result']['resultscore']['textstring'];
651
                            }
652
                            break;
653
                        case self::EXT_WRITE:
654
                        case self::EXT_DELETE:
655
                            $response = true;
656
                            break;
657
                    }
658
                }
659
            }
660
            if (is_array($response) && (count($response) <= 0)) {
661
                $response = '';
662
            }
663
        }
664
 
665
        return $response;
666
 
667
    }
668
 
669
/**
670
 * Perform a Memberships service request.
671
 *
672
 * The user table is updated with the new list of user objects.
673
 *
674
 * @param boolean $withGroups True is group information is to be requested as well
675
 *
676
 * @return mixed Array of User objects or False if the request was not successful
677
 */
678
    public function doMembershipsService($withGroups = false)
679
    {
680
 
681
        $users = array();
682
        $oldUsers = $this->getUserResultSourcedIDs(true, ToolProvider::ID_SCOPE_RESOURCE);
683
        $this->extResponse = null;
684
        $url = $this->getSetting('ext_ims_lis_memberships_url');
685
        $params = array();
686
        $params['id'] = $this->getSetting('ext_ims_lis_memberships_id');
687
        $ok = false;
688
        if ($withGroups) {
689
            $ok = $this->doService('basic-lis-readmembershipsforcontextwithgroups', $url, $params);
690
        }
691
        if ($ok) {
692
            $this->groupSets = array();
693
            $this->groups = array();
694
        } else {
695
            $ok = $this->doService('basic-lis-readmembershipsforcontext', $url, $params);
696
        }
697
 
698
        if ($ok) {
699
            if (!isset($this->extNodes['memberships']['member'])) {
700
                $members = array();
701
            } else if (!isset($this->extNodes['memberships']['member'][0])) {
702
                $members = array();
703
                $members[0] = $this->extNodes['memberships']['member'];
704
            } else {
705
                $members = $this->extNodes['memberships']['member'];
706
            }
707
 
708
            for ($i = 0; $i < count($members); $i++) {
709
 
710
                $user = User::fromResourceLink($this, $members[$i]['user_id']);
711
 
712
// Set the user name
713
                $firstname = (isset($members[$i]['person_name_given'])) ? $members[$i]['person_name_given'] : '';
714
                $lastname = (isset($members[$i]['person_name_family'])) ? $members[$i]['person_name_family'] : '';
715
                $fullname = (isset($members[$i]['person_name_full'])) ? $members[$i]['person_name_full'] : '';
716
                $user->setNames($firstname, $lastname, $fullname);
717
 
718
// Set the user email
719
                $email = (isset($members[$i]['person_contact_email_primary'])) ? $members[$i]['person_contact_email_primary'] : '';
720
                $user->setEmail($email, $this->getConsumer()->defaultEmail);
721
 
722
/// Set the user roles
723
                if (isset($members[$i]['roles'])) {
724
                    $user->roles = ToolProvider::parseRoles($members[$i]['roles']);
725
                }
726
 
727
// Set the user groups
728
                if (!isset($members[$i]['groups']['group'])) {
729
                    $groups = array();
730
                } else if (!isset($members[$i]['groups']['group'][0])) {
731
                    $groups = array();
732
                    $groups[0] = $members[$i]['groups']['group'];
733
                } else {
734
                    $groups = $members[$i]['groups']['group'];
735
                }
736
                for ($j = 0; $j < count($groups); $j++) {
737
                    $group = $groups[$j];
738
                    if (isset($group['set'])) {
739
                        $set_id = $group['set']['id'];
740
                        if (!isset($this->groupSets[$set_id])) {
741
                            $this->groupSets[$set_id] = array('title' => $group['set']['title'], 'groups' => array(),
742
                               'num_members' => 0, 'num_staff' => 0, 'num_learners' => 0);
743
                        }
744
                        $this->groupSets[$set_id]['num_members']++;
745
                        if ($user->isStaff()) {
746
                            $this->groupSets[$set_id]['num_staff']++;
747
                        }
748
                        if ($user->isLearner()) {
749
                            $this->groupSets[$set_id]['num_learners']++;
750
                        }
751
                        if (!in_array($group['id'], $this->groupSets[$set_id]['groups'])) {
752
                            $this->groupSets[$set_id]['groups'][] = $group['id'];
753
                        }
754
                        $this->groups[$group['id']] = array('title' => $group['title'], 'set' => $set_id);
755
                    } else {
756
                        $this->groups[$group['id']] = array('title' => $group['title']);
757
                    }
758
                    $user->groups[] = $group['id'];
759
                }
760
 
761
// If a result sourcedid is provided save the user
762
                if (isset($members[$i]['lis_result_sourcedid'])) {
763
                    $user->ltiResultSourcedId = $members[$i]['lis_result_sourcedid'];
764
                    $user->save();
765
                }
766
                $users[] = $user;
767
 
768
// Remove old user (if it exists)
769
                unset($oldUsers[$user->getId(ToolProvider::ID_SCOPE_RESOURCE)]);
770
            }
771
 
772
// Delete any old users which were not in the latest list from the tool consumer
773
            foreach ($oldUsers as $id => $user) {
774
                $user->delete();
775
            }
776
        } else {
777
            $users = false;
778
        }
779
 
780
        return $users;
781
 
782
    }
783
 
784
/**
785
 * Perform a Setting service request.
786
 *
787
 * @param int    $action The action type constant
788
 * @param string $value  The setting value (optional, default is null)
789
 *
790
 * @return mixed The setting value for a read action, true if a write or delete action was successful, otherwise false
791
 */
792
    public function doSettingService($action, $value = null)
793
    {
794
 
795
        $response = false;
796
        $this->extResponse = null;
797
        switch ($action) {
798
            case self::EXT_READ:
799
                $do = 'basic-lti-loadsetting';
800
                break;
801
            case self::EXT_WRITE:
802
                $do = 'basic-lti-savesetting';
803
                break;
804
            case self::EXT_DELETE:
805
                $do = 'basic-lti-deletesetting';
806
                break;
807
        }
808
        if (isset($do)) {
809
 
810
            $url = $this->getSetting('ext_ims_lti_tool_setting_url');
811
            $params = array();
812
            $params['id'] = $this->getSetting('ext_ims_lti_tool_setting_id');
813
            if (is_null($value)) {
814
                $value = '';
815
            }
816
            $params['setting'] = $value;
817
 
818
            if ($this->doService($do, $url, $params)) {
819
                switch ($action) {
820
                    case self::EXT_READ:
821
                        if (isset($this->extNodes['setting']['value'])) {
822
                            $response = $this->extNodes['setting']['value'];
823
                            if (is_array($response)) {
824
                                $response = '';
825
                            }
826
                        }
827
                        break;
828
                    case self::EXT_WRITE:
829
                        $this->setSetting('ext_ims_lti_tool_setting', $value);
830
                        $this->saveSettings();
831
                        $response = true;
832
                        break;
833
                    case self::EXT_DELETE:
834
                        $response = true;
835
                        break;
836
                }
837
            }
838
        }
839
 
840
        return $response;
841
 
842
    }
843
 
844
/**
845
 * Check if the Tool Settings service is supported.
846
 *
847
 * @return boolean True if this resource link supports the Tool Settings service
848
 */
849
    public function hasToolSettingsService()
850
    {
851
 
852
        $url = $this->getSetting('custom_link_setting_url');
853
 
854
        return !empty($url);
855
 
856
    }
857
 
858
/**
859
 * Get Tool Settings.
860
 *
861
 * @param int      $mode       Mode for request (optional, default is current level only)
862
 * @param boolean  $simple     True if all the simple media type is to be used (optional, default is true)
863
 *
864
 * @return mixed The array of settings if successful, otherwise false
865
 */
866
    public function getToolSettings($mode = Service\ToolSettings::MODE_CURRENT_LEVEL, $simple = true)
867
    {
868
 
869
        $url = $this->getSetting('custom_link_setting_url');
870
        $service = new Service\ToolSettings($this, $url, $simple);
871
        $response = $service->get($mode);
872
 
873
        return $response;
874
 
875
    }
876
 
877
/**
878
 * Perform a Tool Settings service request.
879
 *
880
 * @param array    $settings   An associative array of settings (optional, default is none)
881
 *
882
 * @return boolean True if action was successful, otherwise false
883
 */
884
    public function setToolSettings($settings = array())
885
    {
886
 
887
        $url = $this->getSetting('custom_link_setting_url');
888
        $service = new Service\ToolSettings($this, $url);
889
        $response = $service->set($settings);
890
 
891
        return $response;
892
 
893
    }
894
 
895
/**
896
 * Check if the Membership service is supported.
897
 *
898
 * @return boolean True if this resource link supports the Membership service
899
 */
900
    public function hasMembershipService()
901
    {
902
 
903
        $has = !empty($this->contextId);
904
        if ($has) {
905
            $has = !empty($this->getContext()->getSetting('custom_context_memberships_url'));
906
        }
907
 
908
        return $has;
909
 
910
    }
911
 
912
/**
913
 * Get Memberships.
914
 *
915
 * @return mixed The array of User objects if successful, otherwise false
916
 */
917
    public function getMembership()
918
    {
919
 
920
        $response = false;
921
        if (!empty($this->contextId)) {
922
            $url = $this->getContext()->getSetting('custom_context_memberships_url');
923
            if (!empty($url)) {
924
                $service = new Service\Membership($this, $url);
925
                $response = $service->get();
926
            }
927
        }
928
 
929
        return $response;
930
 
931
    }
932
 
933
/**
934
 * Obtain an array of User objects for users with a result sourcedId.
935
 *
936
 * The array may include users from other resource links which are sharing this resource link.
937
 * It may also be optionally indexed by the user ID of a specified scope.
938
 *
939
 * @param boolean $localOnly True if only users from this resource link are to be returned, not users from shared resource links (optional, default is false)
940
 * @param int     $idScope     Scope to use for ID values (optional, default is null for consumer default)
941
 *
942
 * @return array Array of User objects
943
 */
944
    public function getUserResultSourcedIDs($localOnly = false, $idScope = null)
945
    {
946
 
947
        return $this->getDataConnector()->getUserResultSourcedIDsResourceLink($this, $localOnly, $idScope);
948
 
949
    }
950
 
951
/**
952
 * Get an array of ResourceLinkShare objects for each resource link which is sharing this context.
953
 *
954
 * @return array Array of ResourceLinkShare objects
955
 */
956
    public function getShares()
957
    {
958
 
959
        return $this->getDataConnector()->getSharesResourceLink($this);
960
 
961
    }
962
 
963
/**
964
 * Class constructor from consumer.
965
 *
966
 * @param ToolConsumer $consumer Consumer object
967
 * @param string $ltiResourceLinkId Resource link ID value
968
 * @param string $tempId Temporary Resource link ID value (optional, default is null)
969
 * @return ResourceLink
970
 */
971
    public static function fromConsumer($consumer, $ltiResourceLinkId, $tempId = null)
972
    {
973
 
974
        $resourceLink = new ResourceLink();
975
        $resourceLink->consumer = $consumer;
976
        $resourceLink->dataConnector = $consumer->getDataConnector();
977
        $resourceLink->ltiResourceLinkId = $ltiResourceLinkId;
978
        if (!empty($ltiResourceLinkId)) {
979
            $resourceLink->load();
980
            if (is_null($resourceLink->id) && !empty($tempId)) {
981
                $resourceLink->ltiResourceLinkId = $tempId;
982
                $resourceLink->load();
983
                $resourceLink->ltiResourceLinkId = $ltiResourceLinkId;
984
            }
985
        }
986
 
987
        return $resourceLink;
988
 
989
    }
990
 
991
/**
992
 * Class constructor from context.
993
 *
994
 * @param Context $context Context object
995
 * @param string $ltiResourceLinkId Resource link ID value
996
 * @param string $tempId Temporary Resource link ID value (optional, default is null)
997
 * @return ResourceLink
998
 */
999
    public static function fromContext($context, $ltiResourceLinkId, $tempId = null)
1000
    {
1001
 
1002
        $resourceLink = new ResourceLink();
1003
        $resourceLink->setContextId($context->getRecordId());
1004
        $resourceLink->context = $context;
1005
        $resourceLink->dataConnector = $context->getDataConnector();
1006
        $resourceLink->ltiResourceLinkId = $ltiResourceLinkId;
1007
        if (!empty($ltiResourceLinkId)) {
1008
            $resourceLink->load();
1009
            if (is_null($resourceLink->id) && !empty($tempId)) {
1010
                $resourceLink->ltiResourceLinkId = $tempId;
1011
                $resourceLink->load();
1012
                $resourceLink->ltiResourceLinkId = $ltiResourceLinkId;
1013
            }
1014
        }
1015
 
1016
        return $resourceLink;
1017
 
1018
    }
1019
 
1020
/**
1021
 * Load the resource link from the database.
1022
 *
1023
 * @param int $id     Record ID of resource link
1024
 * @param DataConnector   $dataConnector    Database connection object
1025
 *
1026
 * @return ResourceLink  ResourceLink object
1027
 */
1028
    public static function fromRecordId($id, $dataConnector)
1029
    {
1030
 
1031
        $resourceLink = new ResourceLink();
1032
        $resourceLink->dataConnector = $dataConnector;
1033
        $resourceLink->load($id);
1034
 
1035
        return $resourceLink;
1036
 
1037
    }
1038
 
1039
###
1040
###  PRIVATE METHODS
1041
###
1042
 
1043
/**
1044
 * Load the resource link from the database.
1045
 *
1046
 * @param int $id     Record ID of resource link (optional, default is null)
1047
 *
1048
 * @return boolean True if resource link was successfully loaded
1049
 */
1050
    private function load($id = null)
1051
    {
1052
 
1053
        $this->initialize();
1054
        $this->id = $id;
1055
 
1056
        return $this->getDataConnector()->loadResourceLink($this);
1057
 
1058
    }
1059
 
1060
/**
1061
 * Convert data type of value to a supported type if possible.
1062
 *
1063
 * @param Outcome     $ltiOutcome     Outcome object
1064
 * @param string[]    $supportedTypes Array of outcome types to be supported (optional, default is null to use supported types reported in the last launch for this resource link)
1065
 *
1066
 * @return boolean True if the type/value are valid and supported
1067
 */
1068
    private function checkValueType($ltiOutcome, $supportedTypes = null)
1069
    {
1070
 
1071
        if (empty($supportedTypes)) {
1072
            $supportedTypes = explode(',', str_replace(' ', '', strtolower($this->getSetting('ext_ims_lis_resultvalue_sourcedids', self::EXT_TYPE_DECIMAL))));
1073
        }
1074
        $type = $ltiOutcome->type;
1075
        $value = $ltiOutcome->getValue();
1076
// Check whether the type is supported or there is no value
1077
        $ok = in_array($type, $supportedTypes) || (strlen($value) <= 0);
1078
        if (!$ok) {
1079
// Convert numeric values to decimal
1080
            if ($type === self::EXT_TYPE_PERCENTAGE) {
1081
                if (substr($value, -1) === '%') {
1082
                    $value = substr($value, 0, -1);
1083
                }
1084
                $ok = is_numeric($value) && ($value >= 0) && ($value <= 100);
1085
                if ($ok) {
1086
                    $ltiOutcome->setValue($value / 100);
1087
                    $ltiOutcome->type = self::EXT_TYPE_DECIMAL;
1088
                }
1089
            } else if ($type === self::EXT_TYPE_RATIO) {
1090
                $parts = explode('/', $value, 2);
1091
                $ok = (count($parts) === 2) && is_numeric($parts[0]) && is_numeric($parts[1]) && ($parts[0] >= 0) && ($parts[1] > 0);
1092
                if ($ok) {
1093
                    $ltiOutcome->setValue($parts[0] / $parts[1]);
1094
                    $ltiOutcome->type = self::EXT_TYPE_DECIMAL;
1095
                }
1096
// Convert letter_af to letter_af_plus or text
1097
            } else if ($type === self::EXT_TYPE_LETTER_AF) {
1098
                if (in_array(self::EXT_TYPE_LETTER_AF_PLUS, $supportedTypes)) {
1099
                    $ok = true;
1100
                    $ltiOutcome->type = self::EXT_TYPE_LETTER_AF_PLUS;
1101
                } else if (in_array(self::EXT_TYPE_TEXT, $supportedTypes)) {
1102
                    $ok = true;
1103
                    $ltiOutcome->type = self::EXT_TYPE_TEXT;
1104
                }
1105
// Convert letter_af_plus to letter_af or text
1106
            } else if ($type === self::EXT_TYPE_LETTER_AF_PLUS) {
1107
                if (in_array(self::EXT_TYPE_LETTER_AF, $supportedTypes) && (strlen($value) === 1)) {
1108
                    $ok = true;
1109
                    $ltiOutcome->type = self::EXT_TYPE_LETTER_AF;
1110
                } else if (in_array(self::EXT_TYPE_TEXT, $supportedTypes)) {
1111
                    $ok = true;
1112
                    $ltiOutcome->type = self::EXT_TYPE_TEXT;
1113
                }
1114
// Convert text to decimal
1115
            } else if ($type === self::EXT_TYPE_TEXT) {
1116
                $ok = is_numeric($value) && ($value >= 0) && ($value <=1);
1117
                if ($ok) {
1118
                    $ltiOutcome->type = self::EXT_TYPE_DECIMAL;
1119
                } else if (substr($value, -1) === '%') {
1120
                    $value = substr($value, 0, -1);
1121
                    $ok = is_numeric($value) && ($value >= 0) && ($value <=100);
1122
                    if ($ok) {
1123
                        if (in_array(self::EXT_TYPE_PERCENTAGE, $supportedTypes)) {
1124
                            $ltiOutcome->type = self::EXT_TYPE_PERCENTAGE;
1125
                        } else {
1126
                            $ltiOutcome->setValue($value / 100);
1127
                            $ltiOutcome->type = self::EXT_TYPE_DECIMAL;
1128
                        }
1129
                    }
1130
                }
1131
            }
1132
        }
1133
 
1134
        return $ok;
1135
 
1136
    }
1137
 
1138
/**
1139
 * Send a service request to the tool consumer.
1140
 *
1141
 * @param string $type   Message type value
1142
 * @param string $url    URL to send request to
1143
 * @param array  $params Associative array of parameter values to be passed
1144
 *
1145
 * @return boolean True if the request successfully obtained a response
1146
 */
1147
    private function doService($type, $url, $params)
1148
    {
1149
 
1150
        $ok = false;
1151
        $this->extRequest = null;
1152
        $this->extRequestHeaders = '';
1153
        $this->extResponse = null;
1154
        $this->extResponseHeaders = '';
1155
        if (!empty($url)) {
1156
            $params = $this->getConsumer()->signParameters($url, $type, $this->getConsumer()->ltiVersion, $params);
1157
// Connect to tool consumer
1158
            $http = new HTTPMessage($url, 'POST', $params);
1159
// Parse XML response
1160
            if ($http->send()) {
1161
                $this->extResponse = $http->response;
1162
                $this->extResponseHeaders = $http->responseHeaders;
1163
                try {
1164
                    $this->extDoc = new DOMDocument();
1165
                    $this->extDoc->loadXML($http->response);
1166
                    $this->extNodes = $this->domnodeToArray($this->extDoc->documentElement);
1167
                    if (isset($this->extNodes['statusinfo']['codemajor']) && ($this->extNodes['statusinfo']['codemajor'] === 'Success')) {
1168
                        $ok = true;
1169
                    }
1170
                } catch (\Exception $e) {
1171
                }
1172
            }
1173
            $this->extRequest = $http->request;
1174
            $this->extRequestHeaders = $http->requestHeaders;
1175
        }
1176
 
1177
        return $ok;
1178
 
1179
    }
1180
 
1181
/**
1182
 * Send a service request to the tool consumer.
1183
 *
1184
 * @param string $type Message type value
1185
 * @param string $url  URL to send request to
1186
 * @param string $xml  XML of message request
1187
 *
1188
 * @return boolean True if the request successfully obtained a response
1189
 */
1190
    private function doLTI11Service($type, $url, $xml)
1191
    {
1192
 
1193
        $ok = false;
1194
        $this->extRequest = null;
1195
        $this->extRequestHeaders = '';
1196
        $this->extResponse = null;
1197
        $this->extResponseHeaders = '';
1198
        if (!empty($url)) {
1199
            $id = uniqid();
1200
            $xmlRequest = <<< EOD
1201
<?xml version = "1.0" encoding = "UTF-8"?>
1202
<imsx_POXEnvelopeRequest xmlns = "http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0">
1203
  <imsx_POXHeader>
1204
    <imsx_POXRequestHeaderInfo>
1205
      <imsx_version>V1.0</imsx_version>
1206
      <imsx_messageIdentifier>{$id}</imsx_messageIdentifier>
1207
    </imsx_POXRequestHeaderInfo>
1208
  </imsx_POXHeader>
1209
  <imsx_POXBody>
1210
    <{$type}Request>
1211
{$xml}
1212
    </{$type}Request>
1213
  </imsx_POXBody>
1214
</imsx_POXEnvelopeRequest>
1215
EOD;
1216
// Calculate body hash
1217
            $hash = base64_encode(sha1($xmlRequest, true));
1218
            $params = array('oauth_body_hash' => $hash);
1219
 
1220
// Add OAuth signature
1221
            $hmacMethod = new OAuth\OAuthSignatureMethod_HMAC_SHA1();
1222
            $consumer = new OAuth\OAuthConsumer($this->getConsumer()->getKey(), $this->getConsumer()->secret, null);
1223
            $req = OAuth\OAuthRequest::from_consumer_and_token($consumer, null, 'POST', $url, $params);
1224
            $req->sign_request($hmacMethod, $consumer, null);
1225
            $params = $req->get_parameters();
1226
            $header = $req->to_header();
1227
            $header .= "\nContent-Type: application/xml";
1228
// Connect to tool consumer
1229
            $http = new HTTPMessage($url, 'POST', $xmlRequest, $header);
1230
// Parse XML response
1231
            if ($http->send()) {
1232
                $this->extResponse = $http->response;
1233
                $this->extResponseHeaders = $http->responseHeaders;
1234
                try {
1235
                    $this->extDoc = new DOMDocument();
1236
                    $this->extDoc->loadXML($http->response);
1237
                    $this->extNodes = $this->domnodeToArray($this->extDoc->documentElement);
1238
                    if (isset($this->extNodes['imsx_POXHeader']['imsx_POXResponseHeaderInfo']['imsx_statusInfo']['imsx_codeMajor']) &&
1239
                        ($this->extNodes['imsx_POXHeader']['imsx_POXResponseHeaderInfo']['imsx_statusInfo']['imsx_codeMajor'] === 'success')) {
1240
                        $ok = true;
1241
                    }
1242
                } catch (\Exception $e) {
1243
                }
1244
            }
1245
            $this->extRequest = $http->request;
1246
            $this->extRequestHeaders = $http->requestHeaders;
1247
        }
1248
 
1249
        return $ok;
1250
 
1251
    }
1252
 
1253
/**
1254
 * Convert DOM nodes to array.
1255
 *
1256
 * @param DOMElement $node XML element
1257
 *
1258
 * @return array Array of XML document elements
1259
 */
1260
    private function domnodeToArray($node)
1261
    {
1262
 
1263
        $output = '';
1264
        switch ($node->nodeType) {
1265
            case XML_CDATA_SECTION_NODE:
1266
            case XML_TEXT_NODE:
1267
                $output = trim($node->textContent);
1268
                break;
1269
            case XML_ELEMENT_NODE:
1270
                for ($i = 0; $i < $node->childNodes->length; $i++) {
1271
                    $child = $node->childNodes->item($i);
1272
                    $v = $this->domnodeToArray($child);
1273
                    if (isset($child->tagName)) {
1274
                        $t = $child->tagName;
1275
                        if (!isset($output[$t])) {
1276
                            $output[$t] = array();
1277
                        }
1278
                        $output[$t][] = $v;
1279
                    } else {
1280
                        $s = (string) $v;
1281
                        if (strlen($s) > 0) {
1282
                            $output = $s;
1283
                        }
1284
                    }
1285
                }
1286
                if (is_array($output)) {
1287
                    if ($node->attributes->length) {
1288
                        $a = array();
1289
                        foreach ($node->attributes as $attrName => $attrNode) {
1290
                            $a[$attrName] = (string) $attrNode->value;
1291
                        }
1292
                        $output['@attributes'] = $a;
1293
                    }
1294
                    foreach ($output as $t => $v) {
1295
                        if (is_array($v) && count($v)==1 && $t!='@attributes') {
1296
                            $output[$t] = $v[0];
1297
                        }
1298
                    }
1299
                }
1300
                break;
1301
        }
1302
 
1303
        return $output;
1304
 
1305
    }
1306
 
1307
}