Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 400 and 401] [Versions 401 and 403]

   1  <?php
   2  
   3  namespace Packback\Lti1p3;
   4  
   5  class LtiAssignmentsGradesService extends LtiAbstractService
   6  {
   7      public const CONTENTTYPE_SCORE = 'application/vnd.ims.lis.v1.score+json';
   8      public const CONTENTTYPE_LINEITEM = 'application/vnd.ims.lis.v2.lineitem+json';
   9      public const CONTENTTYPE_LINEITEMCONTAINER = 'application/vnd.ims.lis.v2.lineitemcontainer+json';
  10      public const CONTENTTYPE_RESULTCONTAINER = 'application/vnd.ims.lis.v2.resultcontainer+json';
  11  
  12      public function getScope(): array
  13      {
  14          return $this->getServiceData()['scope'];
  15      }
  16  
  17      // https://www.imsglobal.org/spec/lti-ags/v2p0#assignment-and-grade-service-claim
  18      // When an LTI message is launching a resource associated to one and only one lineitem,
  19      // the claim must include the endpoint URL for accessing the associated line item;
  20      // in all other cases, this property must be either blank or not included in the claim.
  21      public function getResourceLaunchLineItem(): ?LtiLineitem
  22      {
  23          $serviceData = $this->getServiceData();
  24          if (empty($serviceData['lineitem'])) {
  25              return null;
  26          }
  27  
  28          return LtiLineitem::new()->setId($serviceData['lineitem']);
  29      }
  30  
  31      public function putGrade(LtiGrade $grade, LtiLineitem $lineitem = null)
  32      {
  33          if (!in_array(LtiConstants::AGS_SCOPE_SCORE, $this->getScope())) {
  34              throw new LtiException('Missing required scope', 1);
  35          }
  36  
  37          $lineitem = $this->ensureLineItemExists($lineitem);
  38  
  39          $scoreUrl = $lineitem->getId();
  40  
  41          // Place '/scores' before url params
  42          $pos = strpos($scoreUrl, '?');
  43          $scoreUrl = $pos === false ? $scoreUrl.'/scores' : substr_replace($scoreUrl, '/scores', $pos, 0);
  44  
  45          $request = new ServiceRequest(
  46              ServiceRequest::METHOD_POST,
  47              $scoreUrl,
  48              ServiceRequest::TYPE_SYNC_GRADE
  49          );
  50          $request->setBody($grade);
  51          $request->setContentType(static::CONTENTTYPE_SCORE);
  52  
  53          return $this->makeServiceRequest($request);
  54      }
  55  
  56      public function findLineItem(LtiLineitem $newLineItem): ?LtiLineitem
  57      {
  58          $lineitems = $this->getLineItems();
  59  
  60          foreach ($lineitems as $lineitem) {
  61              if ($this->isMatchingLineitem($lineitem, $newLineItem)) {
  62                  return new LtiLineitem($lineitem);
  63              }
  64          }
  65  
  66          return null;
  67      }
  68  
  69      public function updateLineitem(LtiLineItem $lineitemToUpdate): LtiLineitem
  70      {
  71          $request = new ServiceRequest(
  72              ServiceRequest::METHOD_PUT,
  73              $this->getServiceData()['lineitems'],
  74              ServiceRequest::TYPE_UPDATE_LINEITEM
  75          );
  76  
  77          $request->setBody($lineitemToUpdate)
  78              ->setContentType(static::CONTENTTYPE_LINEITEM)
  79              ->setAccept(static::CONTENTTYPE_LINEITEM);
  80  
  81          $updatedLineitem = $this->makeServiceRequest($request);
  82  
  83          return new LtiLineitem($updatedLineitem['body']);
  84      }
  85  
  86      public function createLineitem(LtiLineitem $newLineItem): LtiLineitem
  87      {
  88          $request = new ServiceRequest(
  89              ServiceRequest::METHOD_POST,
  90              $this->getServiceData()['lineitems'],
  91              ServiceRequest::TYPE_CREATE_LINEITEM
  92          );
  93          $request->setBody($newLineItem)
  94              ->setContentType(static::CONTENTTYPE_LINEITEM)
  95              ->setAccept(static::CONTENTTYPE_LINEITEM);
  96          $createdLineItem = $this->makeServiceRequest($request);
  97  
  98          return new LtiLineitem($createdLineItem['body']);
  99      }
 100  
 101      public function findOrCreateLineitem(LtiLineitem $newLineItem): LtiLineitem
 102      {
 103          return $this->findLineItem($newLineItem) ?? $this->createLineitem($newLineItem);
 104      }
 105  
 106      public function getGrades(LtiLineitem $lineitem = null)
 107      {
 108          $lineitem = $this->ensureLineItemExists($lineitem);
 109          $resultsUrl = $lineitem->getId();
 110  
 111          // Place '/results' before url params
 112          $pos = strpos($resultsUrl, '?');
 113          $resultsUrl = $pos === false ? $resultsUrl.'/results' : substr_replace($resultsUrl, '/results', $pos, 0);
 114  
 115          $request = new ServiceRequest(
 116              ServiceRequest::METHOD_GET,
 117              $resultsUrl,
 118              ServiceRequest::TYPE_GET_GRADES
 119          );
 120          $request->setAccept(static::CONTENTTYPE_RESULTCONTAINER);
 121          $scores = $this->makeServiceRequest($request);
 122  
 123          return $scores['body'];
 124      }
 125  
 126      public function getLineItems(): array
 127      {
 128          if (!in_array(LtiConstants::AGS_SCOPE_LINEITEM, $this->getScope())) {
 129              throw new LtiException('Missing required scope', 1);
 130          }
 131  
 132          $request = new ServiceRequest(
 133              ServiceRequest::METHOD_GET,
 134              $this->getServiceData()['lineitems'],
 135              ServiceRequest::TYPE_GET_LINEITEMS
 136          );
 137          $request->setAccept(static::CONTENTTYPE_LINEITEMCONTAINER);
 138  
 139          $lineitems = $this->getAll($request);
 140  
 141          // If there is only one item, then wrap it in an array so the foreach works
 142          if (isset($lineitems['body']['id'])) {
 143              $lineitems['body'] = [$lineitems['body']];
 144          }
 145  
 146          return $lineitems;
 147      }
 148  
 149      public function getLineItem(string $url): LtiLineitem
 150      {
 151          if (!in_array(LtiConstants::AGS_SCOPE_LINEITEM, $this->getScope())) {
 152              throw new LtiException('Missing required scope', 1);
 153          }
 154  
 155          $request = new ServiceRequest(
 156              ServiceRequest::METHOD_GET,
 157              $url,
 158              ServiceRequest::TYPE_GET_LINEITEM
 159          );
 160          $request->setAccept(static::CONTENTTYPE_LINEITEM);
 161  
 162          $response = $this->makeServiceRequest($request)['body'];
 163  
 164          return new LtiLineitem($response);
 165      }
 166  
 167      private function ensureLineItemExists(LtiLineitem $lineitem = null): LtiLineitem
 168      {
 169          // If no line item is passed in, attempt to use the one associated with
 170          // this launch.
 171          if (!isset($lineitem)) {
 172              $lineitem = $this->getResourceLaunchLineItem();
 173          }
 174  
 175          // If none exists still, create a default line item.
 176          if (!isset($lineitem)) {
 177              $defaultLineitem = LtiLineitem::new()
 178                  ->setLabel('default')
 179                  ->setScoreMaximum(100);
 180              $lineitem = $this->createLineitem($defaultLineitem);
 181          }
 182  
 183          // If the line item does not contain an ID, find or create it.
 184          if (empty($lineitem->getId())) {
 185              $lineitem = $this->findOrCreateLineitem($lineitem);
 186          }
 187  
 188          return $lineitem;
 189      }
 190  
 191      private function isMatchingLineitem(array $lineitem, LtiLineitem $newLineItem): bool
 192      {
 193          return $newLineItem->getTag() == ($lineitem['tag'] ?? null) &&
 194              $newLineItem->getResourceId() == ($lineitem['resourceId'] ?? null) &&
 195              $newLineItem->getResourceLinkId() == ($lineitem['resourceLinkId'] ?? null);
 196      }
 197  }