Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

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

   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       * 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  }