Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
/**
18
 * This file contains a class definition for the LTI Gradebook Services
19
 *
20
 * @package    ltiservice_gradebookservices
21
 * @copyright  2017 Cengage Learning http://www.cengage.com
22
 * @author     Dirk Singels, Diego del Blanco, Claude Vervoort
23
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24
 */
25
 
26
namespace ltiservice_gradebookservices\local\service;
27
 
28
use ltiservice_gradebookservices\local\resources\lineitem;
29
use ltiservice_gradebookservices\local\resources\lineitems;
30
use ltiservice_gradebookservices\local\resources\results;
31
use ltiservice_gradebookservices\local\resources\scores;
32
use mod_lti\local\ltiservice\resource_base;
33
use mod_lti\local\ltiservice\service_base;
34
use moodle_url;
35
 
36
defined('MOODLE_INTERNAL') || die();
37
 
38
global $CFG;
39
require_once($CFG->dirroot . '/mod/lti/locallib.php');
40
 
41
/**
42
 * A service implementing LTI Gradebook Services.
43
 *
44
 * @package    ltiservice_gradebookservices
45
 * @copyright  2017 Cengage Learning http://www.cengage.com
46
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
47
 */
48
class gradebookservices extends service_base {
49
 
50
    /** Read-only access to Gradebook services */
51
    const GRADEBOOKSERVICES_READ = 1;
52
    /** Full access to Gradebook services */
53
    const GRADEBOOKSERVICES_FULL = 2;
54
    /** Scope for full access to Lineitem service */
55
    const SCOPE_GRADEBOOKSERVICES_LINEITEM = 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem';
56
    /** Scope for full access to Lineitem service */
57
    const SCOPE_GRADEBOOKSERVICES_LINEITEM_READ = 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly';
58
    /** Scope for access to Result service */
59
    const SCOPE_GRADEBOOKSERVICES_RESULT_READ = 'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly';
60
    /** Scope for access to Score service */
61
    const SCOPE_GRADEBOOKSERVICES_SCORE = 'https://purl.imsglobal.org/spec/lti-ags/scope/score';
62
 
63
 
64
    /**
65
     * Class constructor.
66
     */
67
    public function __construct() {
68
 
69
        parent::__construct();
70
        $this->id = 'gradebookservices';
71
        $this->name = get_string($this->get_component_id(), $this->get_component_id());
72
 
73
    }
74
 
75
    /**
76
     * Get the resources for this service.
77
     *
78
     * @return resource_base[]
79
     */
80
    public function get_resources() {
81
 
82
        // The containers should be ordered in the array after their elements.
83
        // Lineitems should be after lineitem.
84
        if (empty($this->resources)) {
85
            $this->resources = array();
86
            $this->resources[] = new lineitem($this);
87
            $this->resources[] = new lineitems($this);
88
            $this->resources[] = new results($this);
89
            $this->resources[] = new scores($this);
90
        }
91
 
92
        return $this->resources;
93
    }
94
 
95
    /**
96
     * Get the scope(s) permitted for this service.
97
     *
98
     * @return array
99
     */
100
    public function get_permitted_scopes() {
101
 
102
        $scopes = array();
103
        $ok = !empty($this->get_type());
104
        if ($ok && isset($this->get_typeconfig()['ltiservice_gradesynchronization'])) {
105
            if (!empty($setting = $this->get_typeconfig()['ltiservice_gradesynchronization'])) {
106
                $scopes[] = self::SCOPE_GRADEBOOKSERVICES_LINEITEM_READ;
107
                $scopes[] = self::SCOPE_GRADEBOOKSERVICES_RESULT_READ;
108
                $scopes[] = self::SCOPE_GRADEBOOKSERVICES_SCORE;
109
                if ($setting == self::GRADEBOOKSERVICES_FULL) {
110
                    $scopes[] = self::SCOPE_GRADEBOOKSERVICES_LINEITEM;
111
                }
112
            }
113
        }
114
 
115
        return $scopes;
116
 
117
    }
118
 
119
    /**
120
     * Get the scopes defined by this service.
121
     *
122
     * @return array
123
     */
124
    public function get_scopes() {
125
        return [self::SCOPE_GRADEBOOKSERVICES_LINEITEM_READ, self::SCOPE_GRADEBOOKSERVICES_RESULT_READ,
126
            self::SCOPE_GRADEBOOKSERVICES_SCORE, self::SCOPE_GRADEBOOKSERVICES_LINEITEM];
127
    }
128
 
129
    /**
130
     * Adds form elements for gradebook sync add/edit page.
131
     *
132
     * @param \MoodleQuickForm $mform Moodle quickform object definition
133
     */
134
    public function get_configuration_options(&$mform) {
135
 
136
        $selectelementname = 'ltiservice_gradesynchronization';
137
        $identifier = 'grade_synchronization';
138
        $options = [
139
            get_string('nevergs', $this->get_component_id()),
140
            get_string('partialgs', $this->get_component_id()),
141
            get_string('alwaysgs', $this->get_component_id())
142
        ];
143
 
144
        $mform->addElement('select', $selectelementname, get_string($identifier, $this->get_component_id()), $options);
145
        $mform->setType($selectelementname, 'int');
146
        $mform->setDefault($selectelementname, 0);
147
        $mform->addHelpButton($selectelementname, $identifier, $this->get_component_id());
148
    }
149
 
150
    /**
151
     * For submission review, if there is a dedicated URL, use it as the target link.
152
     *
153
     * @param string $messagetype message type for this launch
154
     * @param string $targetlinkuri current target link uri
155
     * @param string|null $customstr concatenated list of custom parameters
156
     * @param int $courseid
157
     * @param null|object $lti LTI Instance.
158
     *
159
     * @return array containing the target link URL and the custom params string to use.
160
     */
161
    public function override_endpoint(string $messagetype, string $targetlinkuri, ?string $customstr, int $courseid,
162
            ?object $lti = null): array {
163
        global $DB;
164
        if ($messagetype == 'LtiSubmissionReviewRequest' && isset($lti->id)) {
165
            $conditions = array('courseid' => $courseid, 'ltilinkid' => $lti->id);
166
            $coupledlineitems = $DB->get_records('ltiservice_gradebookservices', $conditions);
167
            if (count($coupledlineitems) == 1) {
168
                $item = reset($coupledlineitems);
169
                $url = $item->subreviewurl;
170
                $subreviewparams = $item->subreviewparams;
171
                if (!empty($url) && $url != 'DEFAULT') {
172
                    $targetlinkuri = $url;
173
                }
174
                if (!empty($subreviewparams)) {
175
                    if (!empty($customstr)) {
176
                        $customstr .= "\n{$subreviewparams}";
177
                    } else {
178
                        $customstr = $subreviewparams;
179
                    }
180
                }
181
            }
182
        }
183
        return [$targetlinkuri, $customstr];
184
    }
185
 
186
    /**
187
     * Return an array of key/claim mapping allowing LTI 1.1 custom parameters
188
     * to be transformed to LTI 1.3 claims.
189
     *
190
     * @return array Key/value pairs of params to claim mapping.
191
     */
192
    public function get_jwt_claim_mappings(): array {
193
        return [
194
            'custom_gradebookservices_scope' => [
195
                'suffix' => 'ags',
196
                'group' => 'endpoint',
197
                'claim' => 'scope',
198
                'isarray' => true
199
            ],
200
            'custom_lineitems_url' => [
201
                'suffix' => 'ags',
202
                'group' => 'endpoint',
203
                'claim' => 'lineitems',
204
                'isarray' => false
205
            ],
206
            'custom_lineitem_url' => [
207
                'suffix' => 'ags',
208
                'group' => 'endpoint',
209
                'claim' => 'lineitem',
210
                'isarray' => false
211
            ],
212
            'custom_results_url' => [
213
                'suffix' => 'ags',
214
                'group' => 'endpoint',
215
                'claim' => 'results',
216
                'isarray' => false
217
            ],
218
            'custom_result_url' => [
219
                'suffix' => 'ags',
220
                'group' => 'endpoint',
221
                'claim' => 'result',
222
                'isarray' => false
223
            ],
224
            'custom_scores_url' => [
225
                'suffix' => 'ags',
226
                'group' => 'endpoint',
227
                'claim' => 'scores',
228
                'isarray' => false
229
            ],
230
            'custom_score_url' => [
231
                'suffix' => 'ags',
232
                'group' => 'endpoint',
233
                'claim' => 'score',
234
                'isarray' => false
235
            ]
236
        ];
237
    }
238
 
239
    /**
240
     * Return an array of key/values to add to the launch parameters.
241
     *
242
     * @param string $messagetype 'basic-lti-launch-request' or 'ContentItemSelectionRequest'.
243
     * @param string $courseid the course id.
244
     * @param object $user The user id.
245
     * @param string $typeid The tool lti type id.
246
     * @param string $modlti The id of the lti activity.
247
     *
248
     * The type is passed to check the configuration
249
     * and not return parameters for services not used.
250
     *
251
     * @return array of key/value pairs to add as launch parameters.
252
     */
253
    public function get_launch_parameters($messagetype, $courseid, $user, $typeid, $modlti = null) {
254
        global $DB;
255
        $launchparameters = array();
256
        $this->set_type(lti_get_type($typeid));
257
        $this->set_typeconfig(lti_get_type_config($typeid));
258
        // Only inject parameters if the service is enabled for this tool.
259
        if (isset($this->get_typeconfig()['ltiservice_gradesynchronization'])) {
260
            if ($this->get_typeconfig()['ltiservice_gradesynchronization'] == self::GRADEBOOKSERVICES_READ ||
261
                    $this->get_typeconfig()['ltiservice_gradesynchronization'] == self::GRADEBOOKSERVICES_FULL) {
262
                // Check for used in context is only needed because there is no explicit site tool - course relation.
263
                if ($this->is_allowed_in_context($typeid, $courseid)) {
264
                    $id = null;
265
                    if (!is_null($modlti)) {
266
                        $conditions = array('courseid' => $courseid, 'itemtype' => 'mod',
267
                                'itemmodule' => 'lti', 'iteminstance' => $modlti);
268
 
269
                        $coupledlineitems = $DB->get_records('grade_items', $conditions);
270
                        $conditionsgbs = array('courseid' => $courseid, 'ltilinkid' => $modlti);
271
                        $lineitemsgbs = $DB->get_records('ltiservice_gradebookservices', $conditionsgbs);
272
                        // If a link has more that one attached grade items, per spec we do not populate line item url.
273
                        if (count($lineitemsgbs) == 1) {
274
                            $id = reset($lineitemsgbs)->gradeitemid;
275
                        }
276
                        if (count($lineitemsgbs) < 2 && count($coupledlineitems) == 1) {
277
                            $coupledid = reset($coupledlineitems)->id;
278
                            if (!is_null($id) && $id != $coupledid) {
279
                                $id = null;
280
                            } else {
281
                                $id = $coupledid;
282
                            }
283
                        }
284
                    }
285
                    $launchparameters['gradebookservices_scope'] = implode(',', $this->get_permitted_scopes());
286
                    $launchparameters['lineitems_url'] = '$LineItems.url';
287
                    if (!is_null($id)) {
288
                        $launchparameters['lineitem_url'] = '$LineItem.url';
289
                    }
290
                }
291
            }
292
        }
293
        return $launchparameters;
294
    }
295
 
296
    /**
297
     * Fetch the lineitem instances.
298
     *
299
     * @param string $courseid ID of course
300
     * @param string $resourceid Resource identifier used for filtering, may be null
301
     * @param string $ltilinkid Resource Link identifier used for filtering, may be null
302
     * @param string $tag
303
     * @param int $limitfrom Offset for the first line item to include in a paged set
304
     * @param int $limitnum Maximum number of line items to include in the paged set
305
     * @param string $typeid
306
     *
307
     * @return array
308
     * @throws \Exception
309
     */
310
    public function get_lineitems($courseid, $resourceid, $ltilinkid, $tag, $limitfrom, $limitnum, $typeid) {
311
        global $DB;
312
 
313
        // Select all lti potential linetiems in site.
314
        $params = array('courseid' => $courseid);
315
 
316
        $sql = "SELECT i.*
317
                  FROM {grade_items} i
318
                 WHERE (i.courseid = :courseid)
319
               ORDER BY i.id";
320
        $lineitems = $DB->get_records_sql($sql, $params);
321
 
322
        // For each one, check the gbs id, and check that toolproxy matches. If so, add the
323
        // tag to the result and add it to a final results array.
324
        $lineitemstoreturn = array();
325
        $lineitemsandtotalcount = array();
326
        if ($lineitems) {
327
            foreach ($lineitems as $lineitem) {
328
                $gbs = $this->find_ltiservice_gradebookservice_for_lineitem($lineitem->id);
329
                if ($gbs && (!isset($tag) || (isset($tag) && $gbs->tag == $tag))
330
                        && (!isset($ltilinkid) || (isset($ltilinkid) && $gbs->ltilinkid == $ltilinkid))
331
                        && (!isset($resourceid) || (isset($resourceid) && $gbs->resourceid == $resourceid))) {
332
                    if (is_null($typeid)) {
333
                        if ($this->get_tool_proxy()->id == $gbs->toolproxyid) {
334
                            array_push($lineitemstoreturn, $lineitem);
335
                        }
336
                    } else {
337
                        if ($typeid == $gbs->typeid) {
338
                            array_push($lineitemstoreturn, $lineitem);
339
                        }
340
                    }
341
                } else if (($lineitem->itemtype == 'mod' && $lineitem->itemmodule == 'lti'
342
                        && !isset($resourceid) && !isset($tag)
343
                        && (!isset($ltilinkid) || (isset($ltilinkid)
344
                        && $lineitem->iteminstance == $ltilinkid)))) {
345
                    // We will need to check if the activity related belongs to our tool proxy.
346
                    $ltiactivity = $DB->get_record('lti', array('id' => $lineitem->iteminstance));
347
                    if (($ltiactivity) && (isset($ltiactivity->typeid))) {
348
                        if ($ltiactivity->typeid != 0) {
349
                            $tool = $DB->get_record('lti_types', array('id' => $ltiactivity->typeid));
350
                        } else {
351
                            $tool = lti_get_tool_by_url_match($ltiactivity->toolurl, $courseid);
352
                            if (!$tool) {
353
                                $tool = lti_get_tool_by_url_match($ltiactivity->securetoolurl, $courseid);
354
                            }
355
                        }
356
                        if (is_null($typeid)) {
357
                            if (($tool) && ($this->get_tool_proxy()->id == $tool->toolproxyid)) {
358
                                array_push($lineitemstoreturn, $lineitem);
359
                            }
360
                        } else {
361
                            if (($tool) && ($tool->id == $typeid)) {
362
                                array_push($lineitemstoreturn, $lineitem);
363
                            }
364
                        }
365
                    }
366
                }
367
            }
368
            $lineitemsandtotalcount = array();
369
            array_push($lineitemsandtotalcount, count($lineitemstoreturn));
370
            // Return the right array based in the paging parameters limit and from.
371
            if (($limitnum) && ($limitnum > 0)) {
372
                $lineitemstoreturn = array_slice($lineitemstoreturn, $limitfrom, $limitnum);
373
            }
374
            array_push($lineitemsandtotalcount, $lineitemstoreturn);
375
        }
376
        return $lineitemsandtotalcount;
377
    }
378
 
379
    /**
380
     * Fetch a lineitem instance.
381
     *
382
     * Returns the lineitem instance if found, otherwise false.
383
     *
384
     * @param string $courseid ID of course
385
     * @param string $itemid ID of lineitem
386
     * @param string $typeid
387
     *
388
     * @return \ltiservice_gradebookservices\local\resources\lineitem|bool
389
     */
390
    public function get_lineitem($courseid, $itemid, $typeid) {
391
        global $DB, $CFG;
392
 
393
        require_once($CFG->libdir . '/gradelib.php');
394
        $lineitem = \grade_item::fetch(array('id' => $itemid));
395
        if ($lineitem) {
396
            $gbs = $this->find_ltiservice_gradebookservice_for_lineitem($itemid);
397
            if (!$gbs) {
398
                // We will need to check if the activity related belongs to our tool proxy.
399
                $ltiactivity = $DB->get_record('lti', array('id' => $lineitem->iteminstance));
400
                if (($ltiactivity) && (isset($ltiactivity->typeid))) {
401
                    if ($ltiactivity->typeid != 0) {
402
                        $tool = $DB->get_record('lti_types', array('id' => $ltiactivity->typeid));
403
                    } else {
404
                        $tool = lti_get_tool_by_url_match($ltiactivity->toolurl, $courseid);
405
                        if (!$tool) {
406
                            $tool = lti_get_tool_by_url_match($ltiactivity->securetoolurl, $courseid);
407
                        }
408
                    }
409
                    if (is_null($typeid)) {
410
                        if (!(($tool) && ($this->get_tool_proxy()->id == $tool->toolproxyid))) {
411
                            return false;
412
                        }
413
                    } else {
414
                        if (!(($tool) && ($tool->id == $typeid))) {
415
                            return false;
416
                        }
417
                    }
418
                } else {
419
                    return false;
420
                }
421
            }
422
        }
423
        return $lineitem;
424
    }
425
 
426
    /**
427
     * Adds a decoupled (standalone) line item.
428
     * Decoupled line items are not directly attached to
429
     * an lti instance activity. They are recorded in
430
     * the gradebook as manual activities and the
431
     * gradebookservices is used to associate that manual column
432
     * with the tool in addition to storing the LTI related
433
     * metadata (resource id, tag).
434
     *
435
     * @param string $courseid ID of course
436
     * @param string $label label of lineitem
437
     * @param float $maximumscore maximum score of lineitem
438
     * @param string $baseurl
439
     * @param int|null $ltilinkid id of lti instance this line item is associated with
440
     * @param string|null $resourceid resource id of lineitem
441
     * @param string|null $tag tag of lineitem
442
     * @param int $typeid lti type to which this line item is associated with
443
     * @param int|null $toolproxyid lti2 tool proxy to which this lineitem is associated to
444
     *
445
     * @return int id of the created gradeitem
446
     */
447
    public function add_standalone_lineitem(string $courseid, string $label, float $maximumscore,
448
            string $baseurl, ?int $ltilinkid, ?string $resourceid, ?string $tag, int $typeid,
449
            int $toolproxyid = null): int {
450
        global $DB;
451
        $params = array();
452
        $params['itemname'] = $label;
453
        $params['gradetype'] = GRADE_TYPE_VALUE;
454
        $params['grademax']  = $maximumscore;
455
        $params['grademin']  = 0;
456
        $item = new \grade_item(array('id' => 0, 'courseid' => $courseid));
457
        \grade_item::set_properties($item, $params);
458
        $item->itemtype = 'manual';
459
        $item->grademax = $maximumscore;
460
        $id = $item->insert('mod/ltiservice_gradebookservices');
461
        $DB->insert_record('ltiservice_gradebookservices', (object)array(
462
                'gradeitemid' => $id,
463
                'courseid' => $courseid,
464
                'toolproxyid' => $toolproxyid,
465
                'typeid' => $typeid,
466
                'baseurl' => $baseurl,
467
                'ltilinkid' => $ltilinkid,
468
                'resourceid' => $resourceid,
469
                'tag' => $tag
470
        ));
471
        return $id;
472
    }
473
 
474
    /**
475
     * Set a grade item.
476
     *
477
     * @param object $gradeitem Grade Item record
478
     * @param object $score Result object
479
     * @param int $userid User ID
480
     *
481
     * @throws \Exception
482
     * @deprecated since Moodle 3.7 MDL-62599 - please do not use this function any more.
483
     * @see gradebookservices::save_grade_item($gradeitem, $score, $userid)
484
     */
485
    public static function save_score($gradeitem, $score, $userid) {
486
        $service = new gradebookservices();
487
        $service->save_grade_item($gradeitem, $score, $userid);
488
    }
489
 
490
    /**
491
     * Saves a score received from the LTI tool.
492
     *
493
     * @param object $gradeitem Grade Item record
494
     * @param object $score Result object
495
     * @param int $userid User ID
496
     *
497
     * @throws \Exception
498
     */
499
    public function save_grade_item($gradeitem, $score, $userid) {
500
        global $DB, $CFG;
501
        $source = 'mod' . $this->get_component_id();
502
        if ($DB->get_record('user', array('id' => $userid)) === false) {
503
            throw new \Exception(null, 400);
504
        }
505
        require_once($CFG->libdir . '/gradelib.php');
506
        $finalgrade = null;
507
        $timemodified = null;
508
        if (isset($score->scoreGiven)) {
509
            $finalgrade = grade_floatval($score->scoreGiven);
510
            $max = 1;
511
            if (isset($score->scoreMaximum)) {
512
                $max = $score->scoreMaximum;
513
            }
514
            if (!is_null($max) && grade_floats_different($max, $gradeitem->grademax) && grade_floats_different($max, 0.0)) {
515
                // Rescale to match the grade item maximum.
516
                $finalgrade = grade_floatval($finalgrade * $gradeitem->grademax / $max);
517
            }
518
            if (isset($score->timestamp)) {
519
                $timemodified = strtotime($score->timestamp);
520
            } else {
521
                $timemodified = time();
522
            }
523
        }
524
        $feedbackformat = FORMAT_MOODLE;
525
        $feedback = null;
526
        if (!empty($score->comment)) {
527
            $feedback = $score->comment;
528
            $feedbackformat = FORMAT_PLAIN;
529
        }
530
 
531
        if ($gradeitem->is_manual_item()) {
532
            $result = $gradeitem->update_final_grade($userid, $finalgrade, null, $feedback, FORMAT_PLAIN, null, $timemodified);
533
        } else {
534
            if (!$grade = \grade_grade::fetch(array('itemid' => $gradeitem->id, 'userid' => $userid))) {
535
                $grade = new \grade_grade();
536
                $grade->userid = $userid;
537
                $grade->itemid = $gradeitem->id;
538
            }
539
            $grade->rawgrademax = $score->scoreMaximum;
540
            $grade->timemodified = $timemodified;
541
            $grade->feedbackformat = $feedbackformat;
542
            $grade->feedback = $feedback;
543
            $grade->rawgrade = $finalgrade;
544
            $status = grade_update($source, $gradeitem->courseid,
545
                $gradeitem->itemtype, $gradeitem->itemmodule,
546
                $gradeitem->iteminstance, $gradeitem->itemnumber, $grade);
547
 
548
            $result = ($status == GRADE_UPDATE_OK);
549
        }
550
        if (!$result) {
551
            debugging("failed to save score for item ".$gradeitem->id." and user ".$grade->userid);
552
            throw new \Exception(null, 500);
553
        }
554
 
555
    }
556
 
557
    /**
558
     * Get the json object representation of the grade item
559
     *
560
     * @param object $item Grade Item record
561
     * @param string $endpoint Endpoint for lineitems container request
562
     * @param string $typeid
563
     *
564
     * @return object
565
     */
566
    public static function item_for_json($item, $endpoint, $typeid) {
567
 
568
        $lineitem = new \stdClass();
569
        if (is_null($typeid)) {
570
            $typeidstring = "";
571
        } else {
572
            $typeidstring = "?type_id={$typeid}";
573
        }
574
        $lineitem->id = "{$endpoint}/{$item->id}/lineitem" . $typeidstring;
575
        $lineitem->label = $item->itemname;
576
        $lineitem->scoreMaximum = floatval($item->grademax);
577
        $gbs = self::find_ltiservice_gradebookservice_for_lineitem($item->id);
578
        if ($gbs) {
579
            $lineitem->resourceId = (!empty($gbs->resourceid)) ? $gbs->resourceid : '';
580
            $lineitem->tag = (!empty($gbs->tag)) ? $gbs->tag : '';
581
            if (isset($gbs->ltilinkid)) {
582
                $lineitem->resourceLinkId = strval($gbs->ltilinkid);
583
                $lineitem->ltiLinkId = strval($gbs->ltilinkid);
584
            }
585
            if (!empty($gbs->subreviewurl)) {
586
                $submissionreview = new \stdClass();
587
                if ($gbs->subreviewurl != 'DEFAULT') {
588
                    $submissionreview->url = $gbs->subreviewurl;
589
                }
590
                if (!empty($gbs->subreviewparams)) {
591
                    $submissionreview->custom = lti_split_parameters($gbs->subreviewparams);
592
                }
593
                $lineitem->submissionReview = $submissionreview;
594
            }
595
        } else {
596
            $lineitem->tag = '';
597
            if (isset($item->iteminstance)) {
598
                $lineitem->resourceLinkId = strval($item->iteminstance);
599
                $lineitem->ltiLinkId = strval($item->iteminstance);
600
            }
601
        }
602
 
603
        return $lineitem;
604
 
605
    }
606
 
607
    /**
608
     * Get the object matching the JSON representation of the result.
609
     *
610
     * @param object  $grade              Grade record
611
     * @param string  $endpoint           Endpoint for lineitem
612
     * @param int  $typeid                The id of the type to include in the result url.
613
     *
614
     * @return object
615
     */
616
    public static function result_for_json($grade, $endpoint, $typeid) {
617
 
618
        if (is_null($typeid)) {
619
            $id = "{$endpoint}/results?user_id={$grade->userid}";
620
        } else {
621
            $id = "{$endpoint}/results?type_id={$typeid}&user_id={$grade->userid}";
622
        }
623
        $result = new \stdClass();
624
        $result->id = $id;
625
        $result->userId = $grade->userid;
626
        if (!empty($grade->finalgrade)) {
627
            $result->resultScore = floatval($grade->finalgrade);
628
            $result->resultMaximum = floatval($grade->rawgrademax);
629
            if (!empty($grade->feedback)) {
630
                $result->comment = $grade->feedback;
631
            }
632
            if (is_null($typeid)) {
633
                $result->scoreOf = $endpoint;
634
            } else {
635
                $result->scoreOf = "{$endpoint}?type_id={$typeid}";
636
            }
637
            $result->timestamp = date('c', $grade->timemodified);
638
        }
639
        return $result;
640
    }
641
 
642
    /**
643
     * Check if an LTI id is valid.
644
     *
645
     * @param string $linkid             The lti id
646
     * @param string  $course            The course
647
     * @param string  $toolproxy         The tool proxy id
648
     *
649
     * @return boolean
650
     */
651
    public static function check_lti_id($linkid, $course, $toolproxy) {
652
        global $DB;
653
        // Check if lti type is zero or not (comes from a backup).
654
        $sqlparams1 = array();
655
        $sqlparams1['linkid'] = $linkid;
656
        $sqlparams1['course'] = $course;
657
        $ltiactivity = $DB->get_record('lti', array('id' => $linkid, 'course' => $course));
658
        if ($ltiactivity->typeid == 0) {
659
            $tool = lti_get_tool_by_url_match($ltiactivity->toolurl, $course);
660
            if (!$tool) {
661
                $tool = lti_get_tool_by_url_match($ltiactivity->securetoolurl, $course);
662
            }
663
            return (($tool) && ($toolproxy == $tool->toolproxyid));
664
        } else {
665
            $sqlparams2 = array();
666
            $sqlparams2['linkid'] = $linkid;
667
            $sqlparams2['course'] = $course;
668
            $sqlparams2['toolproxy'] = $toolproxy;
669
            $sql = 'SELECT lti.*
670
                      FROM {lti} lti
671
                INNER JOIN {lti_types} typ ON lti.typeid = typ.id
672
                     WHERE lti.id = ?
673
                           AND lti.course = ?
674
                           AND typ.toolproxyid = ?';
675
            return $DB->record_exists_sql($sql, $sqlparams2);
676
        }
677
    }
678
 
679
    /**
680
     * Check if an LTI id is valid when we are in a LTI 1.x case
681
     *
682
     * @param string $linkid             The lti id
683
     * @param string  $course            The course
684
     * @param string  $typeid            The lti type id
685
     *
686
     * @return boolean
687
     */
688
    public static function check_lti_1x_id($linkid, $course, $typeid) {
689
        global $DB;
690
        // Check if lti type is zero or not (comes from a backup).
691
        $sqlparams1 = array();
692
        $sqlparams1['linkid'] = $linkid;
693
        $sqlparams1['course'] = $course;
694
        $ltiactivity = $DB->get_record('lti', array('id' => $linkid, 'course' => $course));
695
        if ($ltiactivity) {
696
            if ($ltiactivity->typeid == 0) {
697
                $tool = lti_get_tool_by_url_match($ltiactivity->toolurl, $course);
698
                if (!$tool) {
699
                    $tool = lti_get_tool_by_url_match($ltiactivity->securetoolurl, $course);
700
                }
701
                return (($tool) && ($typeid == $tool->id));
702
            } else {
703
                $sqlparams2 = array();
704
                $sqlparams2['linkid'] = $linkid;
705
                $sqlparams2['course'] = $course;
706
                $sqlparams2['typeid'] = $typeid;
707
                $sql = 'SELECT lti.*
708
                          FROM {lti} lti
709
                    INNER JOIN {lti_types} typ ON lti.typeid = typ.id
710
                         WHERE lti.id = ?
711
                               AND lti.course = ?
712
                               AND typ.id = ?';
713
                return $DB->record_exists_sql($sql, $sqlparams2);
714
            }
715
        } else {
716
            return false;
717
        }
718
    }
719
 
720
    /**
721
     * Updates the tag, resourceid and submission review values for a grade item coupled to an lti link instance.
722
     *
723
     * @param object $ltiinstance The lti instance to which the grade item is coupled to
724
     * @param string|null $resourceid The resourceid to apply to the lineitem. If empty string which will be stored as null.
725
     * @param string|null $tag The tag to apply to the lineitem. If empty string which will be stored as null.
726
     * @param moodle_url|null $subreviewurl The submission review target link URL
727
     * @param string|null $subreviewparams The submission review custom parameters.
728
     *
729
     */
730
    public static function update_coupled_gradebookservices(object $ltiinstance,
731
            ?string $resourceid, ?string $tag, ?\moodle_url $subreviewurl, ?string $subreviewparams): void {
732
        global $DB;
733
 
734
        if ($ltiinstance && $ltiinstance->typeid) {
735
            $gradeitem = $DB->get_record('grade_items', array('itemmodule' => 'lti', 'iteminstance' => $ltiinstance->id));
736
            if ($gradeitem) {
737
                $resourceid = (isset($resourceid) && empty(trim($resourceid))) ? null : $resourceid;
738
                $subreviewurlstr = $subreviewurl ? $subreviewurl->out(false) : null;
739
                $tag = (isset($tag) && empty(trim($tag))) ? null : $tag;
740
                $gbs = self::find_ltiservice_gradebookservice_for_lineitem($gradeitem->id);
741
                if ($gbs) {
742
                    $gbs->resourceid = $resourceid;
743
                    $gbs->tag = $tag;
744
                    $gbs->subreviewurl = $subreviewurlstr;
745
                    $gbs->subreviewparams = $subreviewparams;
746
                    $DB->update_record('ltiservice_gradebookservices', $gbs);
747
                } else {
748
                    $baseurl = lti_get_type_type_config($ltiinstance->typeid)->lti_toolurl;
749
                    $DB->insert_record('ltiservice_gradebookservices', (object)array(
750
                        'gradeitemid' => $gradeitem->id,
751
                        'courseid' => $gradeitem->courseid,
752
                        'typeid' => $ltiinstance->typeid,
753
                        'baseurl' => $baseurl,
754
                        'ltilinkid' => $ltiinstance->id,
755
                        'resourceid' => $resourceid,
756
                        'tag' => $tag,
757
                        'subreviewurl' => $subreviewurlstr,
758
                        'subreviewparams' => $subreviewparams
759
                    ));
760
                }
761
            }
762
        }
763
    }
764
 
765
    /**
766
     * Called when a new LTI Instance is added.
767
     *
768
     * @param object $lti LTI Instance.
769
     */
770
    public function instance_added(object $lti): void {
771
        self::update_coupled_gradebookservices($lti, $lti->lineitemresourceid ?? null, $lti->lineitemtag ?? null,
772
            isset($lti->lineitemsubreviewurl) ? new moodle_url($lti->lineitemsubreviewurl) : null,
773
            $lti->lineitemsubreviewparams ?? null);
774
    }
775
 
776
    /**
777
     * Called when a new LTI Instance is updated.
778
     *
779
     * @param object $lti LTI Instance.
780
     */
781
    public function instance_updated(object $lti): void {
782
        self::update_coupled_gradebookservices($lti, $lti->lineitemresourceid ?? null, $lti->lineitemtag ?? null,
783
            isset($lti->lineitemsubreviewurl) ? new moodle_url($lti->lineitemsubreviewurl) : null,
784
            $lti->lineitemsubreviewparams ?? null);
785
    }
786
 
787
    /**
788
     * Set the form data when displaying the LTI Instance form.
789
     *
790
     * @param object $defaultvalues Default form values.
791
     */
792
    public function set_instance_form_values(object $defaultvalues): void {
793
        $defaultvalues->lineitemresourceid = '';
794
        $defaultvalues->lineitemtag = '';
795
        $defaultvalues->subreviewurl = '';
796
        $defaultvalues->subreviewparams = '';
797
        if (is_object($defaultvalues) && $defaultvalues->instance) {
798
            $gbs = self::find_ltiservice_gradebookservice_for_lti($defaultvalues->instance);
799
            if ($gbs) {
800
                $defaultvalues->lineitemresourceid = $gbs->resourceid;
801
                $defaultvalues->lineitemtag = $gbs->tag;
802
                $defaultvalues->lineitemsubreviewurl = $gbs->subreviewurl;
803
                $defaultvalues->lineitemsubreviewparams = $gbs->subreviewparams;
804
            }
805
        }
806
    }
807
 
808
    /**
809
     * Deletes orphaned rows from the 'ltiservice_gradebookservices' table.
810
     *
811
     * Sometimes, if a gradebook entry is deleted and it was a lineitem
812
     * the row in the table ltiservice_gradebookservices can become an orphan
813
     * This method will clean these orphans. It will happens based on a task
814
     * because it is not urgent and we don't want to slow the service
815
     */
816
    public static function delete_orphans_ltiservice_gradebookservices_rows() {
817
        global $DB;
818
 
819
        $sql = "DELETE
820
                  FROM {ltiservice_gradebookservices}
821
                 WHERE gradeitemid NOT IN (SELECT id
822
                                             FROM {grade_items} gi)";
823
        $DB->execute($sql);
824
    }
825
 
826
    /**
827
     * Check if a user can be graded in a course
828
     *
829
     * @param int $courseid The course
830
     * @param int $userid The user
831
     * @return bool
832
     */
833
    public static function is_user_gradable_in_course($courseid, $userid) {
834
        global $CFG;
835
 
836
        $gradableuser = false;
837
        $coursecontext = \context_course::instance($courseid);
838
        if (is_enrolled($coursecontext, $userid, '', false)) {
839
            $roles = get_user_roles($coursecontext, $userid);
840
            $gradebookroles = explode(',', $CFG->gradebookroles);
841
            foreach ($roles as $role) {
842
                foreach ($gradebookroles as $gradebookrole) {
843
                    if ($role->roleid === $gradebookrole) {
844
                        $gradableuser = true;
845
                    }
846
                }
847
            }
848
        }
849
 
850
        return $gradableuser;
851
    }
852
 
853
    /**
854
     * Find the right element in the ltiservice_gradebookservice table for an lti instance
855
     *
856
     * @param string $instanceid The LTI module instance id
857
     * @return object gradebookservice for this line item
858
     */
859
    public static function find_ltiservice_gradebookservice_for_lti($instanceid) {
860
        global $DB;
861
 
862
        if ($instanceid) {
863
            $gradeitem = $DB->get_record('grade_items', array('itemmodule' => 'lti', 'iteminstance' => $instanceid));
864
            if ($gradeitem) {
865
                return self::find_ltiservice_gradebookservice_for_lineitem($gradeitem->id);
866
            }
867
        }
868
    }
869
 
870
    /**
871
     * Find the right element in the ltiservice_gradebookservice table for a lineitem
872
     *
873
     * @param string $lineitemid The lineitem (gradeitem) id
874
     * @return object gradebookservice if it exists
875
     */
876
    public static function find_ltiservice_gradebookservice_for_lineitem($lineitemid) {
877
        global $DB;
878
        if ($lineitemid) {
879
            return $DB->get_record('ltiservice_gradebookservices',
880
                    array('gradeitemid' => $lineitemid));
881
        }
882
    }
883
 
884
    /**
885
     * Validates specific ISO 8601 format of the timestamps.
886
     *
887
     * @param string $date The timestamp to check.
888
     * @return boolean true or false if the date matches the format.
889
     *
890
     */
891
    public static function validate_iso8601_date($date) {
892
        if (preg_match('/^([\+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])' .
893
                '(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))' .
894
                '([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24\:?00)([\.,]\d+(?!:))?)?(\17[0-5]\d([\.,]\d+)?)' .
895
                '?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/', $date) > 0) {
896
            return true;
897
        } else {
898
            return false;
899
        }
900
    }
901
}