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 400 and 401] [Versions 400 and 402] [Versions 400 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(LtiServiceConnector::METHOD_POST, $scoreUrl);
  46          $request->setBody($grade);
  47          $request->setContentType(static::CONTENTTYPE_SCORE);
  48  
  49          return $this->makeServiceRequest($request);
  50      }
  51  
  52      public function findLineItem(LtiLineitem $newLineItem): ?LtiLineitem
  53      {
  54          $lineitems = $this->getLineItems();
  55  
  56          foreach ($lineitems as $lineitem) {
  57              if ($this->isMatchingLineitem($lineitem, $newLineItem)) {
  58                  return new LtiLineitem($lineitem);
  59              }
  60          }
  61  
  62          return null;
  63      }
  64  
  65      public function createLineitem(LtiLineitem $newLineItem): LtiLineitem
  66      {
  67          $request = new ServiceRequest(LtiServiceConnector::METHOD_POST, $this->getServiceData()['lineitems']);
  68          $request->setBody($newLineItem)
  69              ->setContentType(static::CONTENTTYPE_LINEITEM)
  70              ->setAccept(static::CONTENTTYPE_LINEITEM);
  71          $createdLineItems = $this->makeServiceRequest($request);
  72  
  73          return new LtiLineitem($createdLineItems['body']);
  74      }
  75  
  76      public function findOrCreateLineitem(LtiLineitem $newLineItem): LtiLineitem
  77      {
  78          return $this->findLineItem($newLineItem) ?? $this->createLineitem($newLineItem);
  79      }
  80  
  81      public function getGrades(LtiLineitem $lineitem = null)
  82      {
  83          $lineitem = $this->ensureLineItemExists($lineitem);
  84          $resultsUrl = $lineitem->getId();
  85  
  86          // Place '/results' before url params
  87          $pos = strpos($resultsUrl, '?');
  88          $resultsUrl = $pos === false ? $resultsUrl.'/results' : substr_replace($resultsUrl, '/results', $pos, 0);
  89  
  90          $request = new ServiceRequest(LtiServiceConnector::METHOD_GET, $resultsUrl);
  91          $request->setAccept(static::CONTENTTYPE_RESULTCONTAINER);
  92          $scores = $this->makeServiceRequest($request);
  93  
  94          return $scores['body'];
  95      }
  96  
  97      public function getLineItems(): array
  98      {
  99          if (!in_array(LtiConstants::AGS_SCOPE_LINEITEM, $this->getScope())) {
 100              throw new LtiException('Missing required scope', 1);
 101          }
 102  
 103          $request = new ServiceRequest(
 104              LtiServiceConnector::METHOD_GET,
 105              $this->getServiceData()['lineitems']
 106          );
 107          $request->setAccept(static::CONTENTTYPE_LINEITEMCONTAINER);
 108  
 109          $lineitems = $this->getAll($request);
 110  
 111          // If there is only one item, then wrap it in an array so the foreach works
 112          if (isset($lineitems['body']['id'])) {
 113              $lineitems['body'] = [$lineitems['body']];
 114          }
 115  
 116          return $lineitems;
 117      }
 118  
 119      public function getLineItem(string $url): LtiLineitem
 120      {
 121          if (!in_array(LtiConstants::AGS_SCOPE_LINEITEM, $this->getScope())) {
 122              throw new LtiException('Missing required scope', 1);
 123          }
 124  
 125          $request = new ServiceRequest(LtiServiceConnector::METHOD_GET, $url);
 126          $request->setAccept(static::CONTENTTYPE_LINEITEM);
 127  
 128          $response = $this->makeServiceRequest($request)['body'];
 129  
 130          return new LtiLineitem($response);
 131      }
 132  
 133      private function ensureLineItemExists(LtiLineitem $lineitem = null): LtiLineitem
 134      {
 135          // If no line item is passed in, attempt to use the one associated with
 136          // this launch.
 137          if (!isset($lineitem)) {
 138              $lineitem = $this->getResourceLaunchLineItem();
 139          }
 140  
 141          // If none exists still, create a default line item.
 142          if (!isset($lineitem)) {
 143              $defaultLineitem = LtiLineitem::new()
 144                  ->setLabel('default')
 145                  ->setScoreMaximum(100);
 146              $lineitem = $this->createLineitem($defaultLineitem);
 147          }
 148  
 149          // If the line item does not contain an ID, find or create it.
 150          if (empty($lineitem->getId())) {
 151              $lineitem = $this->findOrCreateLineitem($lineitem);
 152          }
 153  
 154          return $lineitem;
 155      }
 156  
 157      private function isMatchingLineitem(array $lineitem, LtiLineitem $newLineItem): bool
 158      {
 159          return $newLineItem->getTag() == ($lineitem['tag'] ?? null) &&
 160              $newLineItem->getResourceId() == ($lineitem['resourceId'] ?? null) &&
 161              $newLineItem->getResourceLinkId() == ($lineitem['resourceLinkId'] ?? null);
 162      }
 163  }