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