Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

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