Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

Differences Between: [Versions 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]

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