diff --git a/src/Content/FileItem.php b/src/Content/FileItem.php
index 95a99f9..b3deae5 100644
--- a/src/Content/FileItem.php
+++ b/src/Content/FileItem.php
@@ -3,6 +3,8 @@
namespace ceLTIc\LTI\Content;
+use ceLTIc\LTI\Util;
+
/**
* Class to represent a file content-item object
*
@@ -100,19 +102,27 @@ public function toJsonObject(): object
*
* @param object $item A JSON object representing a file content-item
*
- * @return void
+ * @return bool True if the item is valid
*/
- protected function fromJsonObject(object $item): void
+ protected function fromJsonObject(object $item): bool
{
- parent::fromJsonObject($item);
- foreach (get_object_vars($item) as $name => $value) {
- switch ($name) {
- case 'copyAdvice':
- case 'expiresAt':
- $this->{$name} = $item->{$name};
- break;
+ $ok = parent::fromJsonObject($item);
+ if ($ok) {
+ foreach (get_object_vars($item) as $name => $value) {
+ switch ($name) {
+ case 'copyAdvice':
+ $this->copyAdvice = Util::checkBoolean($item, 'FileItem/copyAdvice');
+ $ok = $ok && !is_null($this->copyAdvice);
+ break;
+ case 'expiresAt':
+ $this->expiresAt = Util::checkBoolean($item, 'FileItem/expiresAt');
+ $ok = $ok && !is_null($this->expiresAt);
+ break;
+ }
}
}
+
+ return $ok;
}
}
diff --git a/src/Content/Image.php b/src/Content/Image.php
index ead4441..145448f 100644
--- a/src/Content/Image.php
+++ b/src/Content/Image.php
@@ -3,6 +3,8 @@
namespace ceLTIc\LTI\Content;
+use ceLTIc\LTI\Util;
+
/**
* Class to represent a content-item image object
*
@@ -89,12 +91,13 @@ public function toJsonObject(): object
/**
* Generate an Image object from its JSON or JSON-LD representation.
*
- * @param object $item A JSON or JSON-LD object representing a content-item
+ * @param object|string $item A JSON or JSON-LD object representing an image or an image URL
*
* @return Image|null The Image object
*/
- public static function fromJsonObject(object $item): ?Image
+ public static function fromJsonObject(object|string $item): ?Image
{
+ $ok = true;
$obj = null;
$width = null;
$height = null;
@@ -104,20 +107,26 @@ public static function fromJsonObject(object $item): ?Image
switch ($name) {
case '@id':
case 'url':
- $url = $item->{$name};
+ $url = Util::checkString($item, "Image/{$name}", false, false, '', false, null);
break;
case 'width':
- $width = $item->width;
+ $width = Util::checkInteger($item, "Image/width", false, 0, true);
+ if (is_null($width)) {
+ $ok = false;
+ }
break;
case 'height':
- $height = $item->height;
+ $height = Util::checkInteger($item, "Image/height", false, 0, true);
+ if (is_null($height)) {
+ $ok = false;
+ }
break;
}
}
} else {
$url = $item;
}
- if ($url) {
+ if ($ok && $url) {
$obj = new Image($url, $width, $height);
}
diff --git a/src/Content/Item.php b/src/Content/Item.php
index 9255f8d..13eaf19 100644
--- a/src/Content/Item.php
+++ b/src/Content/Item.php
@@ -142,7 +142,7 @@ class Item
function __construct(string $type, array|Placement|null $placementAdvices = null, ?string $id = null)
{
$this->type = $type;
- if (!empty($placementAdvices)) {
+ if (!is_null($placementAdvices)) {
if (!is_array($placementAdvices)) {
$placementAdvices = [$placementAdvices];
}
@@ -218,13 +218,16 @@ public function setHtml(?string $html): void
*
* @param Placement|null $placementAdvice Placement advice object
*
- * @return void
+ * @return bool True if a placement was added
*/
- public function addPlacementAdvice(?Placement $placementAdvice): void
+ public function addPlacementAdvice(?Placement $placementAdvice): bool
{
- if (!empty($placementAdvice)) {
+ $ok = !is_null($placementAdvice);
+ if ($ok) {
$this->placements[$placementAdvice->documentTarget] = $placementAdvice;
}
+
+ return $ok;
}
/**
@@ -290,30 +293,32 @@ public static function toJson(array|Item $items, LtiVersion $ltiVersion = LtiVer
}
}
- return json_encode($obj);
+ return json_encode($obj, JSON_UNESCAPED_SLASHES);
}
/**
* Generate an array of Item objects from their JSON representation.
*
- * @param object $items A JSON object representing Content-Items
+ * @param object|array $items A JSON object or array representing Content-Items
*
* @return array Array of Item objects
*/
- public static function fromJson(object $items): array
+ public static function fromJson(object|array $items): array
{
- $isJsonLd = isset($items->{'@graph'});
- if ($isJsonLd) {
- $items = $items->{'@graph'};
- }
- if (!is_array($items)) {
- $items = [$items];
- }
$objs = [];
- foreach ($items as $item) {
- $obj = self::fromJsonItem($item);
- if (!empty($obj)) {
- $objs[] = $obj;
+ if (is_object($items)) {
+ if (isset($items->{'@graph'})) {
+ $items = $items->{'@graph'};
+ }
+ }
+ if (is_array($items)) {
+ foreach ($items as $item) {
+ if (is_object($item)) {
+ $obj = self::fromJsonItem($item);
+ if (!is_null($obj)) {
+ $objs[] = $obj;
+ }
+ }
}
}
@@ -328,10 +333,10 @@ public static function fromJson(object $items): array
protected function toJsonldObject(): object
{
$item = new \stdClass();
- if (!empty($this->id)) {
+ if (!is_null($this->id)) {
$item->{'@id'} = $this->id;
}
- if (!empty($this->type)) {
+ if (!is_null($this->type)) {
if (($this->type === self::TYPE_LTI_LINK) || ($this->type === self::TYPE_LTI_ASSIGNMENT)) {
$item->{'@type'} = 'LtiLinkItem';
} elseif ($this->type === self::TYPE_FILE) {
@@ -342,43 +347,43 @@ protected function toJsonldObject(): object
} else {
$item->{'@type'} = 'ContentItem';
}
- if (!empty($this->title)) {
+ if (!is_null($this->title)) {
$item->title = $this->title;
}
- if (!empty($this->text)) {
+ if (!is_null($this->text)) {
$item->text = $this->text;
- } elseif (!empty($this->html)) {
+ } elseif (!is_null($this->html)) {
$item->text = $this->html;
}
- if (!empty($this->url)) {
+ if (!is_null($this->url)) {
$item->url = $this->url;
}
- if (!empty($this->mediaType)) {
+ if (!is_null($this->mediaType)) {
$item->mediaType = $this->mediaType;
}
- if (!empty($this->placements)) {
+ if (!is_null($this->placements)) {
$placementAdvice = new \stdClass();
$placementAdvices = [];
foreach ($this->placements as $placement) {
$obj = $placement->toJsonldObject();
- if (!empty($obj)) {
- if (!empty($placement->documentTarget)) {
+ if (!is_null($obj)) {
+ if (!is_null($placement->documentTarget)) {
$placementAdvices[] = $placement->documentTarget;
}
$placementAdvice = (object) array_merge((array) $placementAdvice, (array) $obj);
}
}
- if (!empty($placementAdvice)) {
+ if (!is_null($placementAdvice)) {
$item->placementAdvice = $placementAdvice;
- if (!empty($placementAdvices)) {
+ if (!is_null($placementAdvices)) {
$item->placementAdvice->presentationDocumentTarget = implode(',', $placementAdvices);
}
}
}
- if (!empty($this->icon)) {
+ if (!is_null($this->icon)) {
$item->icon = $this->icon->toJsonldObject();
}
- if (!empty($this->thumbnail)) {
+ if (!is_null($this->thumbnail)) {
$item->thumbnail = $this->thumbnail->toJsonldObject();
}
if (!is_null($this->hideOnCreate)) {
@@ -404,9 +409,9 @@ protected function toJsonObject(): object
$item->type = self::TYPE_FILE;
break;
case 'ContentItem':
- if (empty($this->url)) {
+ if (is_null($this->url)) {
$item->type = self::TYPE_HTML;
- } elseif (!empty($this->mediaType) && str_starts_with($this->mediaType, 'image')) {
+ } elseif (!is_null($this->mediaType) && str_starts_with($this->mediaType, 'image')) {
$item->type = self::TYPE_IMAGE;
} else {
$item->type = self::TYPE_LINK;
@@ -416,16 +421,16 @@ protected function toJsonObject(): object
$item->type = $this->type;
break;
}
- if (!empty($this->title)) {
+ if (!is_null($this->title)) {
$item->title = $this->title;
}
- if (!empty($this->text)) {
+ if (!is_null($this->text)) {
$item->text = Util::stripHtml($this->text);
}
- if (!empty($this->html)) {
+ if (!is_null($this->html)) {
$item->html = $this->html;
}
- if (!empty($this->url)) {
+ if (!is_null($this->url)) {
$item->url = $this->url;
}
foreach ($this->placements as $type => $placement) {
@@ -436,14 +441,14 @@ protected function toJsonObject(): object
Placement::TYPE_FRAME => $placement->toJsonObject(),
default => null
};
- if (!empty($obj)) {
+ if (!is_null($obj)) {
$item->{$type} = $obj;
}
}
- if (!empty($this->icon)) {
+ if (!is_null($this->icon)) {
$item->icon = $this->icon->toJsonObject();
}
- if (!empty($this->thumbnail)) {
+ if (!is_null($this->thumbnail)) {
$item->thumbnail = $this->thumbnail->toJsonObject();
}
if (!is_null($this->hideOnCreate)) {
@@ -458,46 +463,58 @@ protected function toJsonObject(): object
*
* @param object $item A JSON or JSON-LD object representing a content-item
*
- * @return Item|LtiLinkItem|FileItem The content-item object
+ * @return Item|LtiLinkItem|FileItem|null The content-item object
*/
- public static function fromJsonItem(object $item): Item|LtiLinkItem|FileItem
+ public static function fromJsonItem(object $item): Item|LtiLinkItem|FileItem|null
{
$obj = null;
$placement = null;
- if (isset($item->{'@type'})) {
- if (isset($item->presentationDocumentTarget)) {
+ $type = Util::checkString($item, 'Item/@type', false, true,
+ ['ContentItem', 'LtiLinkItem', 'AssignmentLinkItem', 'FileItem'], false, null);
+ if (!is_null($type)) {
+ if (isset($item->presentationDocumentTarget) && is_object($item->presentationDocumentTarget)) {
$placement = Placement::fromJsonObject($item, $item->presentationDocumentTarget);
}
- $obj = match ($item->{'@type'}) {
+ $obj = match ($type) {
'ContentItem' => new Item('ContentItem', $placement),
'LtiLinkItem' => new LtiLinkItem($placement),
- 'FileItem' => new FileItem($placement)
+ 'FileItem' => new FileItem($placement),
+ default => new Item($type, $placement),
};
- } elseif (isset($item->type)) {
- $placements = [];
- $placement = Placement::fromJsonObject($item, 'embed');
- if (!empty($placement)) {
- $placements[] = $placement;
- }
- $placement = Placement::fromJsonObject($item, 'iframe');
- if (!empty($placement)) {
- $placements[] = $placement;
- }
- $placement = Placement::fromJsonObject($item, 'window');
- if (!empty($placement)) {
- $placements[] = $placement;
+ } else {
+ $type = Util::checkString($item, 'Item/type', true, true, '', false, null);
+ if (!is_null($type)) {
+ if (!in_array($type, [self::TYPE_LINK, self::TYPE_LTI_LINK, self::TYPE_FILE, self::TYPE_HTML, self::TYPE_IMAGE])) {
+ Util::setMessage(false, "Value of the 'Item/type' element not recognised ('{$type}' found)");
+ }
+ $placements = [];
+ $placement = Placement::fromJsonObject($item, 'embed');
+ if (!is_null($placement)) {
+ $placements[] = $placement;
+ }
+ $placement = Placement::fromJsonObject($item, 'iframe');
+ if (!is_null($placement)) {
+ $placements[] = $placement;
+ }
+ $placement = Placement::fromJsonObject($item, 'window');
+ if (!is_null($placement)) {
+ $placements[] = $placement;
+ }
+ $obj = match ($type) {
+ self::TYPE_LINK,
+ self::TYPE_HTML,
+ self::TYPE_IMAGE => new Item($type, $placements),
+ self::TYPE_LTI_LINK => new LtiLinkItem($placements),
+ self::TYPE_LTI_ASSIGNMENT => new LtiAssignmentItem($placements),
+ self::TYPE_FILE => new FileItem($placements),
+ default => new Item($type, $placements),
+ };
}
- $obj = match ($item->type) {
- self::TYPE_LINK,
- self::TYPE_HTML,
- self::TYPE_IMAGE => new Item($item->type, $placements),
- self::TYPE_LTI_LINK => new LtiLinkItem($placements),
- self::TYPE_LTI_ASSIGNMENT => new LtiAssignmentItem($placements),
- self::TYPE_FILE => new FileItem($placements)
- };
}
- if (!empty($obj)) {
- $obj->fromJsonObject($item);
+ if (!is_null($obj)) {
+ if (!$obj->fromJsonObject($item)) {
+ $obj = null;
+ }
}
return $obj;
@@ -508,13 +525,12 @@ public static function fromJsonItem(object $item): Item|LtiLinkItem|FileItem
*
* @param object $item A JSON object representing a content-item
*
- * @return void
+ * @return bool True if the item is valid
*/
- protected function fromJsonObject(object $item): void
+ protected function fromJsonObject(object $item): bool
{
- if (isset($item->{'@id'})) {
- $this->id = $item->{'@id'};
- }
+ $ok = true;
+ $this->id = Util::checkString($item, 'Item/@id', false, true, '', false, $this->id);
foreach (get_object_vars($item) as $name => $value) {
switch ($name) {
case 'title':
@@ -522,23 +538,48 @@ protected function fromJsonObject(object $item): void
case 'html':
case 'url':
case 'mediaType':
+ $this->{$name} = Util::checkString($item, "Item/{$name}", false, false, '', false, null);
+ if (is_null($this->{$name})) {
+ $ok = false;
+ }
+ break;
case 'hideOnCreate':
- $this->{$name} = $item->{$name};
+ $this->hideOnCreate = Util::checkBoolean($item, 'Item/hideOnCreate');
+ if (is_null($this->hideOnCreate)) {
+ $ok = false;
+ }
break;
case 'placementAdvice':
- $this->addPlacementAdvice(Placement::fromJsonObject($item));
+ $placements = Util::checkString($value, 'Item/placementAdvice/presentationDocumentTarget', false, true, '',
+ false, null);
+ if (!is_null($placements)) {
+ $placements = explode(',', $placements);
+ foreach ($placements as $placement) {
+ $ok = $ok && $this->addPlacementAdvice(Placement::fromJsonObject($item, $placement));
+ }
+ }
break;
case 'embed':
case 'window':
case 'iframe':
- $this->addPlacementAdvice(Placement::fromJsonObject($item, $name));
+ $ok = $ok && $this->addPlacementAdvice(Placement::fromJsonObject($item, $name));
break;
case 'icon':
case 'thumbnail':
- $this->{$name} = Image::fromJsonObject($item->{$name});
+ if (is_object($value) || is_string($value)) {
+ $this->{$name} = Image::fromJsonObject($value);
+ if (is_null($this->{$name})) {
+ $ok = false;
+ }
+ } else {
+ $ok = false;
+ Util::setMessage(true, "The {$name} element must be a simple object or string");
+ }
break;
}
}
+
+ return $ok;
}
}
diff --git a/src/Content/LineItem.php b/src/Content/LineItem.php
index 514624f..f626101 100644
--- a/src/Content/LineItem.php
+++ b/src/Content/LineItem.php
@@ -4,6 +4,7 @@
namespace ceLTIc\LTI\Content;
use ceLTIc\LTI\SubmissionReview;
+use ceLTIc\LTI\Util;
/**
* Class to represent a line-item object
@@ -81,14 +82,14 @@ public function toJsonldObject(): object
$lineItem->{'@type'} = 'LineItem';
$lineItem->label = $this->label;
$lineItem->reportingMethod = 'http://purl.imsglobal.org/ctx/lis/v2p1/Result#normalScore';
- if (!empty($this->resourceId)) {
+ if (!is_null($this->resourceId)) {
$lineItem->assignedActivity = (object) ['activityId' => $this->resourceId];
}
$lineItem->scoreConstraints = (object) [
'@type' => 'NumericLimits',
'normalMaximum' => $this->scoreMaximum
];
- if (!empty($this->submissionReview)) {
+ if (!is_null($this->submissionReview)) {
$lineItem->submissionReview = $this->submissionReview->toJsonObject();
}
@@ -106,13 +107,13 @@ public function toJsonObject(): object
$lineItem->label = $this->label;
$lineItem->scoreMaximum = $this->scoreMaximum;
- if (!empty($this->resourceId)) {
+ if (!is_null($this->resourceId)) {
$lineItem->resourceId = $this->resourceId;
}
- if (!empty($this->tag)) {
+ if (!is_null($this->tag)) {
$lineItem->tag = $this->tag;
}
- if (!empty($this->submissionReview)) {
+ if (!is_null($this->submissionReview)) {
$lineItem->submissionReview = $this->submissionReview->toJsonObject();
}
@@ -135,33 +136,42 @@ public static function fromJsonObject(object $item): ?LineItem
$activityId = null;
$tag = null;
$submissionReview = null;
+ $hasLabel = false;
+ $hasScoreMaximum = false;
foreach (get_object_vars($item) as $name => $value) {
switch ($name) {
case 'label':
- $label = $item->label;
+ $hasLabel = true;
+ $label = Util::checkString($item, 'LineItem/label', false, true, '', false, $label);
break;
case 'reportingMethod':
- $reportingMethod = $item->reportingMethod;
+ $reportingMethod = Util::checkString($item, 'LineItem/reportingMethod', false, true, '', false, $reportingMethod);
break;
case 'scoreConstraints':
- $scoreConstraints = $item->scoreConstraints;
+ if (is_object($value)) {
+ $scoreConstraints = $value;
+ }
break;
case 'scoreMaximum':
- $scoreMaximum = $item->scoreMaximum;
+ $hasScoreMaximum = true;
+ $scoreMaximum = Util::checkNumber($item, 'LineItem/scoreMaximum', false, 0, true);
break;
case 'assignedActivity':
- if (isset($item->assignedActivity->activityId)) {
- $activityId = $item->assignedActivity->activityId;
+ if (isset($item->assignedActivity)) {
+ $activityId = Util::checkString($item->assignedActivity, 'LineItem/assignedActivity/activityId', false,
+ true, '', false, $activityId);
}
break;
case 'resourceId':
- $activityId = $item->resourceId;
+ $activityId = Util::checkString($item, 'LineItem/resourceId', false, true, '', false, $activityId);
break;
case 'tag':
- $tag = $item->tag;
+ $tag = Util::checkString($item, 'LineItem/tag', false, true, '', false, $tag);
break;
case 'submissionReview':
- $submissionReview = SubmissionReview::fromJsonObject($item->submissionReview);
+ if (is_object($item->submissionReview)) {
+ $submissionReview = SubmissionReview::fromJsonObject($item->submissionReview);
+ }
break;
}
}
@@ -169,13 +179,20 @@ public static function fromJsonObject(object $item): ?LineItem
foreach (get_object_vars($scoreConstraints) as $name => $value) {
$method = str_replace('Maximum', 'Score', $name);
if (str_ends_with($reportingMethod, $method)) {
- $scoreMaximum = $value;
+ $scoreMaximum = Util::checkNumber($scoreConstraints, "LineItem/scoreConstraints/{$name}", false, 0, true);
break;
}
}
}
- if (!is_null($scoreMaximum)) {
+ if (!is_null($label) && !is_null($scoreMaximum)) {
$obj = new LineItem($label, $scoreMaximum, $activityId, $tag, $submissionReview);
+ } else {
+ if (!$hasLabel) {
+ Util::setMessage(true, 'A line item must have a label');
+ }
+ if (!$hasScoreMaximum) {
+ Util::setMessage(true, 'A line item must have a maximum score');
+ }
}
return $obj;
diff --git a/src/Content/LtiLinkItem.php b/src/Content/LtiLinkItem.php
index a06948c..67265b0 100644
--- a/src/Content/LtiLinkItem.php
+++ b/src/Content/LtiLinkItem.php
@@ -3,6 +3,8 @@
namespace ceLTIc\LTI\Content;
+use ceLTIc\LTI\Util;
+
/**
* Class to represent an LTI link content-item object
*
@@ -70,11 +72,11 @@ function __construct(array|Placement|null $placementAdvices = null, ?string $id
*/
public function addCustom(string $name, ?string $value = null): void
{
- if (!empty($name)) {
- if (!empty($value)) {
+ if (!is_null($name) && (strlen($name) > 0)) {
+ if (is_string($value)) {
$this->custom[$name] = $value;
} else {
- reset($this->custom[$name]);
+ unset($this->custom[$name]);
}
}
}
@@ -82,11 +84,11 @@ public function addCustom(string $name, ?string $value = null): void
/**
* Set a line-item for the content-item.
*
- * @param LineItem $lineItem Line-item
+ * @param LineItem|null $lineItem Line-item
*
* @return void
*/
- public function setLineItem(LineItem $lineItem): void
+ public function setLineItem(?LineItem $lineItem): void
{
$this->lineItem = $lineItem;
}
@@ -135,7 +137,7 @@ public function setNoUpdate(?bool $noUpdate): void
public function toJsonldObject(): object
{
$item = parent::toJsonldObject();
- if (!empty($this->lineItem)) {
+ if (!is_null($this->lineItem)) {
$item->lineItem = $this->lineItem->toJsonldObject();
}
if (!is_null($this->noUpdate)) {
@@ -162,7 +164,7 @@ public function toJsonldObject(): object
public function toJsonObject(): object
{
$item = parent::toJsonObject();
- if (!empty($this->lineItem)) {
+ if (!is_null($this->lineItem)) {
$item->lineItem = $this->lineItem->toJsonObject();
}
if (!is_null($this->noUpdate)) {
@@ -186,32 +188,62 @@ public function toJsonObject(): object
*
* @param object $item A JSON object representing an LTI link content-item
*
- * @return void
+ * @return bool True if the item is valid
*/
- protected function fromJsonObject(object $item): void
+ protected function fromJsonObject(object $item): bool
{
- parent::fromJsonObject($item);
+ $ok = parent::fromJsonObject($item);
foreach (get_object_vars($item) as $name => $value) {
switch ($name) {
case 'custom':
- foreach ($item->custom as $paramName => $paramValue) {
- $this->addCustom($paramName, $paramValue);
+ $obj = Util::checkObject($item, 'LtiLink/custom', false, true);
+ if (!is_null($obj)) {
+ foreach (\get_object_vars($obj) as $elementName => $elementValue) {
+ $this->addCustom($elementName, $elementValue);
+ }
+ } else {
+ $ok = false;
}
break;
case 'lineItem':
- $this->setLineItem(LineItem::fromJsonObject($item->lineItem));
+ if (is_object($item->lineItem)) {
+ $lineItem = LineItem::fromJsonObject($item->lineItem);
+ if (!is_null($lineItem)) {
+ $this->setLineItem($lineItem);
+ } else {
+ $ok = false;
+ }
+ } elseif (isset($item->lineItem)) {
+ $ok = false;
+ Util::setMessage(true, 'The lineItem element must be a simple object');
+ }
break;
case 'available':
- $this->setAvailable(TimePeriod::fromJsonObject($item->available));
+ if (is_object($item->available)) {
+ $this->setAvailable(TimePeriod::fromJsonObject($item->available));
+ } elseif (isset($item->available)) {
+ $ok = false;
+ Util::setMessage(true, 'The available element must be a simple object');
+ }
break;
case 'submission':
- $this->setSubmission(TimePeriod::fromJsonObject($item->submission));
+ if (is_object($item->submission)) {
+ $this->setSubmission(TimePeriod::fromJsonObject($item->submission));
+ } elseif (isset($item->submission)) {
+ $ok = false;
+ Util::setMessage(true, 'The submission element must be a simple object');
+ }
break;
case 'noUpdate':
- $this->noUpdate = $item->noUpdate;
+ $this->noUpdate = Util::checkBoolean($item, 'LtiLink/noUpdate');
+ if (is_null($this->noUpdate) && isset($this->noUpdate)) {
+ $ok = false;
+ }
break;
}
}
+
+ return $ok;
}
}
diff --git a/src/Content/Placement.php b/src/Content/Placement.php
index 1b82803..22e4d6c 100644
--- a/src/Content/Placement.php
+++ b/src/Content/Placement.php
@@ -3,6 +3,8 @@
namespace ceLTIc\LTI\Content;
+use ceLTIc\LTI\Util;
+
/**
* Class to represent a content-item placement object
*
@@ -199,6 +201,7 @@ public function toJsonObject(): ?object
*/
public static function fromJsonObject(object $item, ?string $documentTarget = null): ?Placement
{
+ $ok = true;
$obj = null;
$displayWidth = null;
$displayHeight = null;
@@ -208,23 +211,22 @@ public static function fromJsonObject(object $item, ?string $documentTarget = nu
$html = null;
if (isset($item->{'@type'})) { // Version 1
if (empty($documentTarget) && isset($item->placementAdvice)) {
- if (isset($item->placementAdvice->presentationDocumentTarget)) {
- $documentTarget = $item->placementAdvice->presentationDocumentTarget;
- }
+ $documentTarget = Util::checkString($item->placementAdvice, 'Item/placementAdvice/presentationDocumentTarget',
+ false, true, ['embed', 'frame', 'iframe', 'none', 'overlay', 'popup', 'window'], false, null);
+ $ok = $ok && (!is_null($documentTarget) || isset($item->placementAdvice->presentationDocumentTarget));
}
- if (!empty($documentTarget) && isset($item->placementAdvice)) {
- if (isset($item->placementAdvice->displayWidth)) {
- $displayWidth = $item->placementAdvice->displayWidth;
- }
- if (isset($item->placementAdvice->displayHeight)) {
- $displayHeight = $item->placementAdvice->displayHeight;
- }
- if (isset($item->placementAdvice->windowTarget)) {
- $windowTarget = $item->placementAdvice->windowTarget;
- }
+ if (!empty($documentTarget) && isset($item->placementAdvice) && is_object($item->placementAdvice)) {
+ $displayWidth = Util::checkInteger($item->placementAdvice, 'Item/placementAdvice/displayWidth', false, 0, true);
+ $ok = $ok && (!is_null($displayWidth) || !isset($item->placementAdvice->displayWidth));
+ $displayHeight = Util::checkInteger($item->placementAdvice, 'Item/placementAdvice/displayHeight', false, 0, true);
+ $ok = $ok && (!is_null($displayHeight) || !isset($item->placementAdvice->displayHeight));
+ $windowTarget = Util::checkString($item->placementAdvice, 'Item/placementAdvice/windowTarget', false, true, '',
+ false, null);
+ $ok = $ok && (!is_null($windowTarget) || !isset($item->placementAdvice->windowTarget));
}
if (isset($item->url)) {
- $url = $item->url;
+ $url = Util::checkString($item, 'url', false, true, '', false, null);
+ $ok = $ok && !is_null($url);
}
} else { // Version 2
if (empty($documentTarget)) {
@@ -239,27 +241,23 @@ public static function fromJsonObject(object $item, ?string $documentTarget = nu
$documentTarget = null;
}
if (!empty($documentTarget)) {
- if (isset($item->{$documentTarget}->width)) {
- $displayWidth = $item->{$documentTarget}->width;
- }
- if (isset($item->{$documentTarget}->height)) {
- $displayHeight = $item->{$documentTarget}->height;
- }
- if (isset($item->{$documentTarget}->targetName)) {
- $windowTarget = $item->{$documentTarget}->targetName;
- }
- if (isset($item->{$documentTarget}->windowFeatures)) {
- $windowFeatures = $item->{$documentTarget}->windowFeatures;
- }
- if (isset($item->{$documentTarget}->src)) {
- $url = $item->{$documentTarget}->src;
- }
- if (isset($item->{$documentTarget}->html)) {
- $html = $item->{$documentTarget}->html;
- }
+ $displayWidth = Util::checkInteger($item->{$documentTarget}, "Item/{$documentTarget}/width", false, 0, true);
+ $ok = $ok && (!is_null($displayWidth) || !isset($item->{$documentTarget}->width));
+ $displayHeight = Util::checkInteger($item->{$documentTarget}, "Item/{$documentTarget}/height", false, 0, true);
+ $ok = $ok && (!is_null($displayHeight) || !isset($item->{$documentTarget}->height));
+ $windowTarget = Util::checkString($item->{$documentTarget}, "Item/{$documentTarget}/targetName", false, true, '',
+ false, null);
+ $ok = $ok && (!is_null($windowTarget) || !isset($item->{$documentTarget}->targetName));
+ $windowFeatures = Util::checkString($item->{$documentTarget}, "Item/{$documentTarget}/windowFeatures", false, true,
+ '', false, null);
+ $ok = $ok && (!is_null($windowFeatures) || !isset($item->{$documentTarget}->windowFeatures));
+ $url = Util::checkString($item->{$documentTarget}, "Item/{$documentTarget}/src", false, true, '', false, $url);
+ $ok = $ok && (!is_null($url) || !isset($item->{$documentTarget}->src));
+ $html = Util::checkString($item->{$documentTarget}, "Item/{$documentTarget}/html", false, true, '', false, $html);
+ $ok = $ok && (!is_null($html) || !isset($item->{$documentTarget}->html));
}
}
- if (!empty($documentTarget)) {
+ if ($ok && !empty($documentTarget)) {
$obj = new Placement($documentTarget, $displayWidth, $displayHeight, $windowTarget, $windowFeatures, $url, $html);
}
diff --git a/src/Content/TimePeriod.php b/src/Content/TimePeriod.php
index 2b20ba5..1cd9b08 100644
--- a/src/Content/TimePeriod.php
+++ b/src/Content/TimePeriod.php
@@ -3,6 +3,8 @@
namespace ceLTIc\LTI\Content;
+use ceLTIc\LTI\Util;
+
/**
* Class to represent a time period object
*
@@ -88,12 +90,24 @@ public static function fromJsonObject(object $item): ?TimePeriod
$startDateTime = null;
$endDateTime = null;
foreach (get_object_vars($item) as $name => $value) {
+ if (!Util::$strictMode) {
+ $value = Util::valToString($value);
+ }
+ if (!is_string($value)) {
+ continue;
+ }
switch ($name) {
case 'startDateTime':
- $startDateTime = strtotime($item->startDateTime);
+ $startDateTime = strtotime($value);
+ if ($startDateTime === false) {
+ $startDateTime = null;
+ }
break;
case 'endDateTime':
- $endDateTime = strtotime($item->endDateTime);
+ $endDateTime = strtotime($value);
+ if ($endDateTime === false) {
+ $endDateTime = null;
+ }
break;
}
}
diff --git a/src/LineItem.php b/src/LineItem.php
index 61776e4..b4fd788 100644
--- a/src/LineItem.php
+++ b/src/LineItem.php
@@ -195,7 +195,9 @@ public function deleteOutcome(User $user): bool
*/
public static function fromEndpoint(Platform $platform, string $endpoint): LineItem|bool
{
- return Service\LineItem::getLineItem($platform, $endpoint);
+ $lineItemService = new Service\LineItem($platform, $endpoint);
+
+ return lineItemService->get();
}
}
diff --git a/src/OAuth/OAuthUtil.php b/src/OAuth/OAuthUtil.php
index 2d34e75..712419a 100644
--- a/src/OAuth/OAuthUtil.php
+++ b/src/OAuth/OAuthUtil.php
@@ -203,6 +203,14 @@ public static function build_http_query(?array $params): string
return implode('&', $pairs);
}
+ /**
+ * Recursively merge two arrays.
+ *
+ * @param array $array1 First array
+ * @param array $array2 Second array
+ *
+ * @return array
+ */
public static function array_merge_recursive(array $array1, array $array2): array
{
$array = [];
diff --git a/src/OAuthDataStore.php b/src/OAuthDataStore.php
index bc831e8..f179683 100644
--- a/src/OAuthDataStore.php
+++ b/src/OAuthDataStore.php
@@ -101,7 +101,7 @@ function lookup_nonce(OAuthConsumer $consumer, OAuthToken $token, string $value,
$ok = $nonce->save();
}
if (!$ok) {
- $this->system->reason = 'Invalid nonce.';
+ $this->system->setReason('Invalid nonce');
}
return !$ok;
diff --git a/src/Platform.php b/src/Platform.php
index bff679c..10260a9 100644
--- a/src/Platform.php
+++ b/src/Platform.php
@@ -9,6 +9,7 @@
use ceLTIc\LTI\Enum\IdScope;
use ceLTIc\LTI\Enum\LogLevel;
use ceLTIc\LTI\ApiHook\ApiHook;
+use ceLTIc\LTI\Content\Item;
/**
* Class to represent a platform
@@ -429,7 +430,7 @@ public function hasAccessTokenService(): bool
public function getMessageParameters(): ?array
{
if ($this->ok && is_null($this->messageParameters)) {
- $this->parseMessage(true, true, false);
+ $this->parseMessage(true, false);
}
return $this->messageParameters;
@@ -438,9 +439,11 @@ public function getMessageParameters(): ?array
/**
* Process an incoming request
*
+ * @param bool $generateWarnings True if warning messages should be generated (optional, default is false)
+ *
* @return void
*/
- public function handleRequest(): void
+ public function handleRequest(bool $generateWarnings = false): void
{
$parameters = Util::getRequestParameters();
if ($this->debugMode) {
@@ -453,7 +456,10 @@ public function handleRequest(): void
} else { // LTI message
$this->getMessageParameters();
Util::logRequest();
- if ($this->ok && $this->authenticate()) {
+ if ($this->ok) {
+ $this->authenticate($generateWarnings);
+ }
+ if ($this->ok) {
$this->doCallback();
}
}
@@ -760,7 +766,7 @@ protected function onAuthenticate(): void
*/
protected function onContentItem(): void
{
- $this->reason = 'No onContentItem method found for platform';
+ $this->setReason('No onContentItem method found for platform');
$this->onError();
}
@@ -771,7 +777,7 @@ protected function onContentItem(): void
*/
protected function onLtiStartAssessment(): void
{
- $this->reason = 'No onLtiStartAssessment method found for platform';
+ $this->setReason('No onLtiStartAssessment method found for platform');
$this->onError();
}
@@ -794,16 +800,36 @@ protected function onError(): void
*
* The platform, resource link and user objects will be initialised if the request is valid.
*
- * @return bool True if the request has been successfully validated.
+ * @param bool $generateWarnings True if warning messages should be generated (optional, default is false)
+ *
+ * @return void
*/
- private function authenticate(): bool
+ private function authenticate(bool $generateWarnings = false): void
{
- $this->ok = $this->checkMessage();
+ $this->checkMessage($generateWarnings);
+ if (($this->ok || $generateWarnings) && !empty($this->messageParameters['lti_message_type'])) {
+ if ($this->messageParameters['lti_message_type'] === 'ContentItemSelection') {
+ if (isset($this->messageParameters['content_items'])) {
+ $value = Util::jsonDecode($this->messageParameters['content_items']);
+ if (is_null($value)) {
+ $this->setReason('Invalid JSON in \'content_items\' parameter');
+ } elseif (empty($this->jwt) || !$this->jwt->hasJwt()) {
+ if (is_object($value)) {
+ Item::fromJson($value);
+ } else {
+ $this->setReason('\'content_items\' parameter must be an object');
+ }
+ } elseif (is_array($value)) {
+ Item::fromJson($value);
+ } else {
+ $this->setReason('\'content_items\' parameter must be an array');
+ }
+ }
+ }
+ }
if ($this->ok) {
- $this->ok = $this->verifySignature();
+ $this->verifySignature();
}
-
- return $this->ok;
}
/**
diff --git a/src/ResourceLink.php b/src/ResourceLink.php
index ceb85e1..cba1ec1 100644
--- a/src/ResourceLink.php
+++ b/src/ResourceLink.php
@@ -683,15 +683,15 @@ public function doOutcomesService(ServiceAction $action, Outcome $ltiOutcome, Us
}
}
if (!empty($resultDataType)) {
- $xml = <<< EOF
+ $xml = <<< EOD
<{$resultDataType}>{$comment}{$resultDataType}>
-EOF;
+EOD;
}
}
- $xml = <<< EOF
+ $xml = <<< EOD
@@ -699,16 +699,16 @@ public function doOutcomesService(ServiceAction $action, Outcome $ltiOutcome, Us
{$outcome}
{$xml}
-EOF;
+EOD;
}
$sourcedId = htmlentities($sourcedId);
- $xml = <<
{$sourcedId}
{$xml}
-EOF;
+EOD;
if ($this->doLTI11Service($do, $urlLTI11, $xml)) {
switch ($action) {
case ServiceAction::Read:
@@ -1459,15 +1459,17 @@ private function doService(string $type, string $url, array $params, string $sco
$this->extResponseHeaders = $http->responseHeaders;
try {
$this->extDoc = new DOMDocument();
- $this->extDoc->loadXML($http->response);
- $this->extNodes = $this->domnodeToArray($this->extDoc->documentElement);
- if (isset($this->extNodes['statusinfo']['codemajor']) && ($this->extNodes['statusinfo']['codemajor'] === 'Success')) {
- $ok = true;
+ @$this->extDoc->loadXML($http->response);
+ if ($this->extDoc->documentElement) {
+ $this->extNodes = $this->domnodeToArray($this->extDoc->documentElement);
+ if (isset($this->extNodes['statusinfo']['codemajor']) && ($this->extNodes['statusinfo']['codemajor'] === 'Success')) {
+ $ok = true;
+ }
+ } else {
+ Util::setMessage(true, 'Invalid XML in service response');
}
} catch (\Exception $e) {
- } catch (\TypeError $e) {
-
}
}
$retry = $retry && !$newToken && !$ok;
@@ -1620,16 +1622,18 @@ private function doLTI11Service(string $type, string $url, string $xml): bool
$this->extResponseHeaders = $http->responseHeaders;
try {
$this->extDoc = new DOMDocument();
- $this->extDoc->loadXML($http->response);
- $this->extNodes = $this->domnodeToArray($this->extDoc->documentElement);
- if (isset($this->extNodes['imsx_POXHeader']['imsx_POXResponseHeaderInfo']['imsx_statusInfo']['imsx_codeMajor']) &&
- ($this->extNodes['imsx_POXHeader']['imsx_POXResponseHeaderInfo']['imsx_statusInfo']['imsx_codeMajor'] === 'success')) {
- $ok = true;
+ @$this->extDoc->loadXML($http->response);
+ if ($this->extDoc->documentElement) {
+ $this->extNodes = $this->domnodeToArray($this->extDoc->documentElement);
+ if (isset($this->extNodes['imsx_POXHeader']['imsx_POXResponseHeaderInfo']['imsx_statusInfo']['imsx_codeMajor']) &&
+ ($this->extNodes['imsx_POXHeader']['imsx_POXResponseHeaderInfo']['imsx_statusInfo']['imsx_codeMajor'] === 'success')) {
+ $ok = true;
+ }
+ } else {
+ Util::setMessage(true, 'Invalid XML in service response');
}
} catch (\Exception $e) {
- } catch (\TypeError $e) {
-
}
}
$retry = $retry && !$newToken && !$ok;
diff --git a/src/Service/Groups.php b/src/Service/Groups.php
index 9062cad..4952861 100644
--- a/src/Service/Groups.php
+++ b/src/Service/Groups.php
@@ -5,6 +5,7 @@
use ceLTIc\LTI\Context;
use ceLTIc\LTI\User;
+use ceLTIc\LTI\Util;
/**
* Class to implement the Course Groups service
@@ -153,24 +154,31 @@ public function getGroupSets(?int $limit = null): bool
$endpoint = $this->endpoint;
do {
$http = $this->send('GET', $parameters);
- $ok = !empty($http) && $http->ok;
+ $ok = $http->ok;
$url = '';
if ($ok) {
- if (isset($http->responseJson->sets)) {
- foreach ($http->responseJson->sets as $set) {
- $groupSets[$set->id] = [
- 'title' => $set->name,
- 'hidden' => false,
+ $sets = Util::checkArray($http->responseJson, 'sets', true);
+ foreach ($sets as $set) {
+ if (!is_object($set)) {
+ Util::setMessage(true,
+ 'The \'sets\' element must comprise an array of objects (' . gettype($set) . ' found)');
+ continue;
+ }
+ $id = Util::checkString($set, 'id', true, true);
+ $name = Util::checkString($set, 'name', true, true);
+ $tag = Util::checkString($set, 'tag', false, true);
+ $hidden = Util::checkBoolean($set, 'hidden', false, false);
+ if (!empty($id) && !empty($name)) {
+ $groupSets[$id] = [
+ 'title' => $name,
+ 'hidden' => $hidden,
'groups' => [],
'num_members' => 0,
'num_staff' => 0,
'num_learners' => 0
];
- if (isset($set->tag)) {
- $groupSets[$set->id]['tag'] = $set->tag;
- }
- if (isset($set->hidden)) {
- $groupSets[$set->id]['hidden'] = $set->hidden;
+ if (!empty($tag)) {
+ $groupSets[$id]['tag'] = $tag;
}
}
}
@@ -231,30 +239,44 @@ public function getGroups(bool $allowNonSets = false, ?User $user = null, ?int $
$endpoint = $this->endpoint;
do {
$http = $this->send('GET', $parameters);
- $ok = !empty($http) && $http->ok;
+ $ok = $http->ok;
$url = '';
if ($ok) {
- if (isset($http->responseJson->groups)) {
- foreach ($http->responseJson->groups as $agroup) {
- if (!$allowNonSets && empty($agroup->set_ids)) {
+ $jsongroups = Util::checkArray($http->responseJson, 'groups', true);
+ foreach ($jsongroups as $agroup) {
+ if (!is_object($agroup)) {
+ Util::setMessage(true,
+ 'The \'groups\' element must comprise an array of objects (' . gettype($agroup) . ' found)');
+ continue;
+ }
+ $id = Util::checkString($agroup, 'groups/id', true, true);
+ $name = Util::checkString($agroup, 'groups/name', true, true);
+ $tag = Util::checkString($agroup, 'groups/tag');
+ $hidden = Util::checkBoolean($agroup, 'hidden', false, false);
+ $setids = Util::checkArray($agroup, 'groups/set_ids');
+ if (!empty($id) && !empty($name)) {
+ if (!$allowNonSets && empty($setids)) {
continue;
}
$group = [
- 'title' => $agroup->name,
- 'hidden' => false
+ 'title' => $name,
+ 'hidden' => $hidden
];
- if (!empty($agroup->set_ids) && is_array(($agroup->set_ids))) {
- foreach ($agroup->set_ids as $set_id) {
- if (!array_key_exists($set_id, $groupSets)) {
- $groupSets[$set_id] = [
- 'title' => "Set {$set_id}",
+ if (!empty($tag)) {
+ $group['tag'] = $tag;
+ }
+ if (!empty($setids)) {
+ foreach ($setids as $setid) {
+ if (!array_key_exists($setid, $groupSets)) {
+ $groupSets[$setid] = [
+ 'title' => "Set {$setid}",
'groups' => [],
'num_members' => 0,
'num_staff' => 0,
'num_learners' => 0
];
}
- $groupSets[$set_id]['groups'][] = $agroup->id;
+ $groupSets[$setid]['groups'][] = $id;
if (!isset($group['set'])) {
$group['set'] = $setid;
} elseif (!is_array($group['set'])) {
@@ -264,13 +286,7 @@ public function getGroups(bool $allowNonSets = false, ?User $user = null, ?int $
}
}
}
- if (!empty($agroup->tag)) {
- $group['tag'] = $agroup->tag;
- }
- if (isset($agroup->hidden)) {
- $group['hidden'] = $agroup->hidden;
- }
- $groups[$agroup->id] = $group;
+ $groups[$id] = $group;
}
}
if (!$this->pagingMode && $http->hasRelativeLink('next')) {
diff --git a/src/Service/LineItem.php b/src/Service/LineItem.php
index 07d7da4..8319fda 100644
--- a/src/Service/LineItem.php
+++ b/src/Service/LineItem.php
@@ -6,6 +6,7 @@
use ceLTIc\LTI;
use ceLTIc\LTI\Platform;
use ceLTIc\LTI\SubmissionReview;
+use ceLTIc\LTI\Util;
/**
* Class to implement the Line-item service
@@ -118,11 +119,18 @@ public function getAll(?string $ltiResourceLinkId = null, ?string $resourceId =
$this->mediaType = self::MEDIA_TYPE_LINE_ITEMS;
$http = $this->send('GET', $params);
$this->scope = self::$SCOPE;
+ $ok = $http->ok && !empty($http->responseJson);
$url = '';
- if ($http->ok) {
- if (!empty($http->responseJson)) {
- foreach ($http->responseJson as $lineItem) {
- $lineItems[] = self::toLineItem($this->getPlatform(), $lineItem);
+ if ($ok) {
+ $items = Util::checkArray($http, 'responseJson');
+ foreach ($items as $lineItemJson) {
+ if (!is_object($lineItemJson)) {
+ Util::setMessage(true, 'The array must comprise an array of objects (' . gettype($lineItemJson) . ' found)');
+ } else {
+ $lineItem = $this->toLineItem($this->getPlatform(), $lineItemJson);
+ if ($lineItem) {
+ $lineItems[] = $lineItem;
+ }
}
}
if (!$this->pagingMode && $http->hasRelativeLink('next')) {
@@ -153,7 +161,7 @@ public function createLineItem(LTI\LineItem $lineItem): bool
$http = $this->send('POST', null, self::toJson($lineItem));
$ok = $http->ok && !empty($http->responseJson);
if ($ok) {
- $newLineItem = self::toLineItem($this->getPlatform(), $http->responseJson);
+ $newLineItem = $this->toLineItem($this->getPlatform(), $http->responseJson);
if ($newLineItem) {
foreach (get_object_vars($newLineItem) as $key => $value) {
$lineItem->$key = $value;
@@ -177,11 +185,13 @@ public function saveLineItem(LTI\LineItem $lineItem): bool
$http = $this->send('PUT', null, self::toJson($lineItem));
$ok = $http->ok;
if ($ok && !empty($http->responseJson)) {
- $savedLineItem = self::toLineItem($this->getPlatform(), $http->responseJson);
+ $savedLineItem = $this->toLineItem($this->getPlatform(), $http->responseJson);
+ if ($savedLineItem) {
foreach (get_object_vars($savedLineItem) as $key => $value) {
$lineItem->$key = $value;
}
}
+ }
return $ok;
}
@@ -204,20 +214,22 @@ public function deleteLineItem(LTI\LineItem $lineItem): bool
/**
* Retrieve a line-item.
*
- * @param Platform $platform Platform object for this service request
- * @param string $endpoint Line-item endpoint
- *
* @return LTI\\LineItem|bool LineItem object, or false on error
*/
- public static function getLineItem(Platform $platform, string $endpoint): LTI\LineItem|bool
+ public function get(): LTI\LineItem|bool
{
- $service = new self($platform, $endpoint);
- $service->scope = self::$SCOPE_READONLY;
- $service->mediaType = self::MEDIA_TYPE_LINE_ITEM;
- $http = $service->send('GET');
- $service->scope = self::$SCOPE;
+ $this->scope = self::$SCOPE_READONLY;
+ $this->mediaType = self::MEDIA_TYPE_LINE_ITEM;
+ $http = $this->send('GET');
if ($http->ok && !empty($http->responseJson)) {
- $lineItem = self::toLineItem($platform, $http->responseJson);
+ if (!is_object($http->responseJson)) {
+ Util::setMessage(true, 'The response must be an object (' . gettype($http->responseJson) . ' found)');
+ } else {
+ $lineItem = $this->toLineItem($this->getPlatform(), $http->responseJson);
+ if (empty($lineItem)) {
+ $lineItem = false;
+ }
+ }
} else {
$lineItem = false;
}
@@ -225,6 +237,21 @@ public static function getLineItem(Platform $platform, string $endpoint): LTI\Li
return $lineItem;
}
+ /**
+ * Retrieve a line-item.
+ *
+ * @deprecated Use LineItem::fromEndpoint() or get() instead
+ *
+ * @param Platform $platform Platform object for this service request
+ * @param string $endpoint Line-item endpoint
+ *
+ * @return LTI\\LineItem|bool LineItem object, or false on error
+ */
+ public static function getLineItem(Platform $platform, string $endpoint): LTI\LineItem|bool
+ {
+ return LTI\LineItem::fromEndpoint($platform, $endpoint);
+ }
+
###
### PRIVATE METHODS
###
@@ -237,30 +264,45 @@ public static function getLineItem(Platform $platform, string $endpoint): LTI\Li
*
* @return LTI\\LineItem|null LineItem object, or null on error
*/
- private static function toLineItem(Platform $platform, object $json): ?LTI\LineItem
+ private function toLineItem(Platform $platform, object $json): ?LTI\LineItem
{
- if (!empty($json->id) && !empty($json->label) && !empty($json->scoreMaximum)) {
- $lineItem = new LTI\LineItem($platform, $json->label, $json->scoreMaximum);
- if (!empty($json->id)) {
- $lineItem->endpoint = $json->id;
+ $id = Util::checkString($json, 'id', true, true);
+ $scoreMaximum = Util::checkNumber($json, 'scoreMaximum', true, 0, true);
+ $label = Util::checkString($json, 'label', true, true);
+ $resourceId = Util::checkString($json, 'resourceId');
+ $tag = Util::checkString($json, 'tag');
+ $startDateTime = Util::checkDateTime($json, 'startDateTime');
+ $endDateTime = Util::checkDateTIme($json, 'endDateTime');
+ $resourceLinkId = Util::checkString($json, 'resourceLinkId');
+ if (!empty($id) && !empty($label) &&
+ (!is_null($scoreMaximum) || (!empty($json->gradingScheme) && is_object($json->gradingScheme)))) {
+ $lineItem = new LTI\LineItem($platform, $label, $scoreMaximum);
+ if (!empty($json->gradingScheme)) {
+ $lineItem->gradingScheme = GradingScheme::fromJsonObject($json->gradingScheme);
}
- if (!empty($json->resourceLinkId)) {
- $lineItem->ltiResourceLinkId = $json->resourceLinkId;
+ $lineItem->endpoint = $json->id;
+ if (!empty($resourceLinkId)) {
+ $lineItem->ltiResourceLinkId = $resourceLinkId;
}
- if (!empty($json->resourceId)) {
- $lineItem->resourceId = $json->resourceId;
+ if (!empty($resourceId)) {
+ $lineItem->resourceId = $resourceId;
}
- if (!empty($json->tag)) {
- $lineItem->tag = $json->tag;
+ if (!empty($tag)) {
+ $lineItem->tag = $tag;
}
- if (!empty($json->startDateTime)) {
- $lineItem->submitFrom = strtotime($json->startDateTime);
+ if (!empty($startDateTime)) {
+ $lineItem->submitFrom = $startDateTime;
}
- if (!empty($json->endDateTime)) {
- $lineItem->submitUntil = strtotime($json->endDateTime);
+ if (!empty($endDateTime)) {
+ $lineItem->submitUntil = $endDateTime;
}
if (!empty($json->submissionReview)) {
+ if (is_object($json->submissionReview)) {
$lineItem->submissionReview = SubmissionReview::fromJsonObject($json->submissionReview);
+ } else {
+ Util::setMessage(true,
+ 'The \'submissionReview\' element must be an object (' . gettype($json->submissionReviewJson) . ' found)');
+ }
}
} else {
$lineItem = null;
diff --git a/src/Service/Membership.php b/src/Service/Membership.php
index 3628cd7..e4eb060 100644
--- a/src/Service/Membership.php
+++ b/src/Service/Membership.php
@@ -8,6 +8,7 @@
use ceLTIc\LTI\ResourceLink;
use ceLTIc\LTI\Enum\IdScope;
use ceLTIc\LTI\Enum\LtiVersion;
+use ceLTIc\LTI\Util;
/**
* Class to implement the Membership service
@@ -151,210 +152,400 @@ private function getMembers(bool $withGroups, ?string $role = null, ?int $limit
$this->source->getGroups();
$parameters['groups'] = 'true';
}
+ $ok = true;
$userResults = [];
$memberships = [];
$endpoint = $this->endpoint;
do {
$http = $this->send('GET', $parameters);
+ $ok = $http->ok;
$url = '';
- if (!empty($http) && $http->ok) {
- $isjsonld = false;
- if (isset($http->responseJson->pageOf) && isset($http->responseJson->pageOf->membershipSubject) &&
- isset($http->responseJson->pageOf->membershipSubject->membership)) {
- $isjsonld = true;
- $memberships = array_merge($memberships, $http->responseJson->pageOf->membershipSubject->membership);
- if (!empty($http->responseJson->nextPage) && !empty($http->responseJson->pageOf->membershipSubject->membership)) {
+ if ($ok) {
+ $members = [];
+ if ($this->mediaType === self::MEDIA_TYPE_MEMBERSHIPS_V1) {
+ if (empty(Util::checkString($http->responseJson, '@type', true, true, 'Page', false))) {
+ $ok = $ok && !Util::$strictMode;
+ }
+ if (!isset($http->responseJson->pageOf)) {
+ $ok = false;
+ Util::setMessage(true, 'The pageOf element is missing');
+ } else {
+ if (empty(Util::checkString($http->responseJson->pageOf, 'pageOf/@type', true, true,
+ 'LISMembershipContainer', false))) {
+ $ok = $ok && !Util::$strictMode;
+ }
+ if (isset($http->responseJson->pageOf->membershipSubject)) {
+ if (empty(Util::checkString($http->responseJson->pageOf->membershipSubject,
+ 'pageOf/membershipSubject/@type', true, true, 'Context', false))) {
+ $ok = $ok && !Util::$strictMode;
+ }
+ if (empty(Util::checkString($http->responseJson->pageOf->membershipSubject,
+ 'pageOf/membershipSubject/contextId'))) {
+ $ok = $ok && !Util::$strictMode;
+ }
+ $members = Util::checkArray($http->responseJson->pageOf->membershipSubject,
+ 'pageOf/membershipSubject/membership');
+ }
+ }
+ if (!empty($http->responseJson->nextPage) && !empty($members)) {
$http->relativeLinks['next'] = $http->responseJson->nextPage;
}
- } elseif (isset($http->responseJson->members) && is_array($http->responseJson->members)) {
- $memberships = array_merge($memberships, $http->responseJson->members);
- }
- if (!$this->pagingMode && $http->hasRelativeLink('next')) {
- $url = $http->getRelativeLink('next');
- $this->endpoint = $url;
- $parameters = [];
+ } else { // Version 2 (NRPS)
+ if (!isset($http->responseJson->context)) {
+ if (Util::$strictMode) {
+ $ok = false;
+ Util::setMessage(true, 'The context element is missing');
+ } else {
+ Util::setMessage(false, 'The context element should be present');
+ }
+ } elseif (empty(Util::checkString($http->responseJson->context, 'context/id'))) {
+ $ok = $ok && !Util::$strictMode;
+ }
+ $members = Util::checkArray($http->responseJson, 'members');
+ if ($ok && !$this->pagingMode && $http->hasRelativeLink('next')) {
+ $url = $http->getRelativeLink('next');
+ $this->endpoint = $url;
+ $parameters = [];
+ }
}
- } else {
- $userResults = false;
+ $memberships = array_merge($memberships, $members);
}
} while ($url);
$this->endpoint = $endpoint;
- if ($userResults !== false) {
+ if ($ok) {
if ($isLink) {
$oldUsers = $this->source->getUserResultSourcedIDs(true, IdScope::Resource);
}
+ if ($this->mediaType === self::MEDIA_TYPE_MEMBERSHIPS_V1) {
+ if (!empty($http->responseJson->{'@context'})) {
+ $contexts = $http->responseJson->{'@context'};
+ if (is_string($contexts)) {
+ $contexts = [$contexts];
+ } elseif (!is_array($contexts)) {
+ $contexts = [];
+ }
+ } else {
+ $contexts = [];
+ }
+ }
foreach ($memberships as $membership) {
- if ($isjsonld) {
- $member = $membership->member;
- if ($isLink) {
- $userResult = LTI\UserResult::fromResourceLink($this->source, $member->userId);
+ if ($this->mediaType === self::MEDIA_TYPE_MEMBERSHIPS_V1) {
+ if (isset($membership->member)) {
+ $member = $membership->member;
} else {
- $userResult = new LTI\UserResult();
- $userResult->ltiUserId = $member->userId;
+ Util::setMessage(true, 'The membership/member element is missing');
+ $member = null;
}
+ if (empty(Util::checkArray($membership, 'membership/role', true, true)) && Util::$strictMode) {
+ $member = null;
+ }
+ if ($isLink) {
+ $messages = Util::checkArray($membership, 'membership/message');
+ }
+ if (!empty($member)) {
+ $userid = null;
+ if (!empty($member->userId)) {
+ $userid = $member->userId;
+ } elseif (!empty($member->{'@id'})) {
+ $userid = $member->{'@id'};
+ }
+ if (empty($userid)) {
+ Util::setMessage(true, 'The membership/member/userid or @id element is missing');
+ } elseif (!is_string($userid)) {
+ if (Util::$strictMode) {
+ Util::setMessage(true, 'The membership/member/userid or @id element must have a string ');
+ $userid = null;
+ } else {
+ Util::setMessage(false, 'The membership/member/userid or @id element should have a string ');
+ $userid = LTI\Util::valToString($userid);
+ }
+ }
+ $roles = [];
+ $stringroles = Util::checkArray($membership, 'membership/role', true, true);
+ foreach ($stringroles as $role) {
+ if (!is_string($role)) {
+ if (Util::$strictMode) {
+ Util::setMessage(true, 'The membership/role element must only comprise string values');
+ $userid = null;
+ } else {
+ Util::setMessage(false, 'The membership/role element should only comprise string values');
+ }
+ } else {
+ $roles[] = $role;
+ }
+ }
+ if (!empty($userid)) {
+ if ($isLink) {
+ $userResult = LTI\UserResult::fromResourceLink($this->source, $userid);
+ } else {
+ $userResult = new LTI\UserResult();
+ $userResult->ltiUserId = $userid;
+ }
// Set the user name
- $firstname = $member->givenName ?? '';
- $middlename = $member->middleName ?? '';
- $lastname = $member->familyName ?? '';
- $fullname = $member->name ?? '';
- $userResult->setNames($firstname, $lastname, $fullname);
+ $firstname = Util::checkString($member, 'membership/member/givenName');
+ $middlename = Util::checkString($member, 'membership/member/middleName');
+ $lastname = Util::checkString($member, 'membership/member/familyName');
+ $fullname = Util::checkString($member, 'membership/member/name');
+ $userResult->setNames($firstname, $lastname, $fullname, $middlename);
// Set the sourcedId
- if (isset($member->sourcedId)) {
- $userResult->sourcedId = $member->sourcedId;
- }
-
-// Set the username
- if (isset($member->ext_username)) {
- $userResult->username = $member->ext_username;
- } elseif (isset($member->ext_user_username)) {
- $userResult->username = $member->ext_user_username;
- } elseif (isset($member->custom_username)) {
- $userResult->username = $member->custom_username;
- } elseif (isset($member->custom_user_username)) {
- $userResult->username = $member->custom_user_username;
- }
+ if (isset($member->sourcedId)) {
+ $userResult->sourcedId = Util::checkString($member, 'membership/member/sourcedId');
+ }
// Set the user email
- $email = $member->email ?? '';
- $userResult->setEmail($email, $this->source->getPlatform()->defaultEmail);
+ $email = Util::checkString($member, 'membership/member/email');
+ $userResult->setEmail($email, $this->source->getPlatform()->defaultEmail);
// Set the user roles
- if (isset($membership->role)) {
- $roles = $this->parseContextsInArray($http->responseJson->{'@context'}, $membership->role);
- $ltiVersion = $this->getPlatform()->ltiVersion;
- if (empty($ltiVersion)) {
- $ltiVersion = LtiVersion::V1;
- }
- $userResult->roles = LTI\Tool::parseRoles($roles, $ltiVersion);
- }
+ if (!empty($roles)) {
+ $roles = $this->parseContextsInArray($contexts, $roles);
+ $ltiVersion = $this->getPlatform()->ltiVersion;
+ if (empty($ltiVersion)) {
+ $ltiVersion = LtiVersion::V1;
+ }
+ $userResult->roles = LTI\Tool::parseRoles($roles, $ltiVersion);
+ }
// If a result sourcedid is provided save the user
- if ($isLink) {
- $doSave = false;
- if (isset($membership->message)) {
- foreach ($membership->message as $message) {
- if (isset($message->message_type) && (($message->message_type === 'basic-lti-launch-request') || ($message->message_type) === 'LtiResourceLinkRequest')) {
- if (isset($message->lis_result_sourcedid)) {
- if (empty($userResult->ltiResultSourcedId) || ($userResult->ltiResultSourcedId !== $message->lis_result_sourcedid)) {
- $userResult->ltiResultSourcedId = $message->lis_result_sourcedid;
- $doSave = true;
- }
- } elseif ($userResult->isLearner() && empty($userResult->created)) { // Ensure all learners are recorded in case Assignment and Grade services are used
- $userResult->ltiResultSourcedId = '';
- $doSave = true;
- }
- if (isset($message->ext)) {
- if (empty($userResult->username)) {
- if (!empty($message->ext->username)) {
- $userResult->username = $message->ext->username;
- } elseif (!empty($message->ext->user_username)) {
- $userResult->username = $message->ext->user_username;
+ if ($isLink) {
+ $doSave = false;
+ if (is_array($messages)) {
+ foreach ($messages as $message) {
+ if (!is_object($message)) {
+ Util::setMessage(true,
+ 'The membership/message element must comprise an array of objects (' . gettype($message) . ' found)');
+ continue;
+ } else {
+ if (isset($message->ext)) {
+ if (!is_object($message->ext)) {
+ Util::setMessage(true,
+ 'The membership/message/ext element must be an object (' . gettype($message->ext) . ' found)');
+ }
+ }
+ if (isset($message->custom)) {
+ if (!is_object($message->custom)) {
+ Util::setMessage(true,
+ 'The membership/message/custom element must be an object (' . gettype($message->custom) . ' found)');
+ }
}
}
- }
- if (isset($message->custom)) {
- if (empty($userResult->username)) {
- if (!empty($message->custom->username)) {
- $userResult->username = $message->custom->username;
- } elseif (!empty($message->custom->user_username)) {
- $userResult->username = $message->custom->user_username;
+ if (!isset($message->message_type)) {
+ Util::setMessage(true,
+ 'The membership/message elements must include a \'message_type\' property');
+ } elseif (($message->message_type === 'basic-lti-launch-request') || ($message->message_type === 'LtiResourceLinkRequest')) {
+ if (isset($message->lis_result_sourcedid)) {
+ $sourcedid = Util::checkString($message, 'membership/message/lis_result_sourcedid');
+ if (empty($userResult->ltiResultSourcedId) || ($userResult->ltiResultSourcedId !== $sourcedid)) {
+ $userResult->ltiResultSourcedId = $sourcedid;
+ $doSave = true;
+ }
+ } elseif ($userResult->isLearner() && empty($userResult->created)) { // Ensure all learners are recorded in case Assignment and Grade services are used
+ $userResult->ltiResultSourcedId = '';
+ $doSave = true;
+ }
+ $username = null;
+ if (is_object($message->ext)) {
+ if (!empty($message->ext->username)) {
+ $username = Util::checkString($message->ext, 'membership/message/ext/username');
+ } elseif (!empty($message->ext->user_username)) {
+ $username = Util::checkString($message->ext,
+ 'membership/message/ext/user_username');
+ }
}
+ if (empty($username) && is_object($message->custom)) {
+ if (!empty($message->custom->username)) {
+ $username = Util::checkString($message->custom,
+ 'membership/message/custom/username');
+ } elseif (!empty($message->custom->user_username)) {
+ $username = Util::checkString($message->custom,
+ 'membership/message/custom/user_username');
+ }
+ }
+ if (!empty($username)) {
+ $userResult->username = $username;
+ }
+ break;
}
}
- break;
+ } elseif ($userResult->isLearner() && empty($userResult->created)) { // Ensure all learners are recorded in case Assignment and Grade services are used
+ $userResult->ltiResultSourcedId = '';
+ $doSave = true;
+ }
+ if (!$doSave && isset($member->resultSourcedId)) {
+ $userResult->setResourceLinkId($this->source->getId());
+ $userResult->ltiResultSourcedId = Util::checkString($member, 'membership/member/resultSourcedId');
+ $doSave = true;
+ }
+ if ($doSave) {
+ $userResult->save();
}
}
- } elseif ($userResult->isLearner() && empty($userResult->created)) { // Ensure all learners are recorded in case Assignment and Grade services are used
- $userResult->ltiResultSourcedId = '';
- $doSave = true;
- }
- if (!$doSave && isset($member->resultSourcedId)) {
- $userResult->setResourceLinkId($this->source->getId());
- $userResult->ltiResultSourcedId = $member->resultSourcedId;
- $doSave = true;
- }
- if ($doSave) {
- $userResult->save();
- }
- }
- $userResults[] = $userResult;
+ $userResults[] = $userResult;
// Remove old user (if it exists)
- if ($isLink) {
- unset($oldUsers[$userResult->getId(IdScope::Resource)]);
+ if ($isLink) {
+ unset($oldUsers[$userResult->getId(IdScope::Resource)]);
+ }
+ }
}
- } else { // Version 2
+ } else { // Version 2 (NRPS)
$member = $membership;
+ $userid = null;
+ $userid = Util::checkString($member, 'members/user_id', true);
+ $roles = [];
+ $stringroles = Util::checkArray($member, 'members/roles', true, true);
+ foreach ($stringroles as $role) {
+ if (!is_string($role)) {
+ if (Util::$strictMode) {
+ Util::setMessage(true, 'The members/roles element must only comprise string values');
+ $userid = null;
+ } else {
+ Util::setMessage(false, 'The members/roles element should only comprise string values');
+ }
+ } else {
+ $roles[] = $role;
+ }
+ }
+ }
+ if ($isLink) {
+ if (isset($member->message)) {
+ $messages = $member->message;
+ if (!is_array($messages)) {
+ if (Util::$strictMode) {
+ $userid = null;
+ Util::setMessage(true,
+ 'The members/message element must have an array value (' . gettype($member->message) . ' found)');
+ $messages = [];
+ } else {
+ Util::setMessage(false,
+ 'The members/message element should have an array value (' . gettype($membership->message) . ' found)');
+ if (is_object($messages)) {
+ $messages = (array) $messages;
+ } else {
+ $messages = [];
+ }
+ }
+ }
+ } else {
+ $messages = [];
+ }
+ }
+ if (!empty($userid)) {
if ($isLink) {
- $userResult = LTI\UserResult::fromResourceLink($this->source, $member->user_id);
+ $userResult = LTI\UserResult::fromResourceLink($this->source, $userid);
} else {
$userResult = new LTI\UserResult();
- $userResult->ltiUserId = $member->user_id;
+ $userResult->ltiUserId = $userid;
}
// Set the user name
- $firstname = $member->given_name ?? '';
- $middlename = $member->middle_name ?? '';
- $lastname = $member->family_name ?? '';
- $fullname = $member->name ?? '';
- $userResult->setNames($firstname, $lastname, $fullname);
+ $firstname = Util::checkString($member, 'members/given_mame');
+ $middlename = Util::checkString($member, 'members/middle_name');
+ $lastname = Util::checkString($member, 'members/family_name');
+ $fullname = Util::checkString($member, 'members/name');
+ $userResult->setNames($firstname, $lastname, $fullname, $middlename);
// Set the sourcedId
if (isset($member->lis_person_sourcedid)) {
- $userResult->sourcedId = $member->lis_person_sourcedid;
+ $userResult->sourcedId = Util::checkString($member, 'members/lis_person_sourcedid');
}
// Set the user email
- $email = $member->email ?? '';
+ $email = Util::checkString($member, 'email');
$userResult->setEmail($email, $this->source->getPlatform()->defaultEmail);
// Set the user roles
- if (isset($member->roles)) {
+ if (!empty($roles)) {
$ltiVersion = $this->getPlatform()->ltiVersion;
if (empty($ltiVersion)) {
$ltiVersion = LtiVersion::V1;
}
- $userResult->roles = LTI\Tool::parseRoles($member->roles, $ltiVersion);
+ $userResult->roles = LTI\Tool::parseRoles($roles, $ltiVersion);
}
// If a result sourcedid is provided save the user
+ $groupenrollments = [];
if ($isLink) {
$doSave = false;
- if (isset($member->message)) {
- $messages = $member->message;
- if (!is_array($messages)) {
- $messages = [$member->message];
- }
+ if (is_array($messages)) {
foreach ($messages as $message) {
- if (isset($message->{'https://purl.imsglobal.org/spec/lti/claim/message_type'}) && (($message->{'https://purl.imsglobal.org/spec/lti/claim/message_type'} === 'basic-lti-launch-request') || ($message->{'https://purl.imsglobal.org/spec/lti/claim/message_type'}) === 'LtiResourceLinkRequest')) {
+ if (!is_object($message)) {
+ Util::setMessage(true,
+ 'The members/message element must comprise an array of objects (' . gettype($message) . ' found)');
+ continue;
+ } else {
+ if (isset($message->{'https://purl.imsglobal.org/spec/lti/claim/ext'})) {
+ if (!is_object($message->{'https://purl.imsglobal.org/spec/lti/claim/ext'})) {
+ Util::setMessage(true,
+ 'The members/message/https://purl.imsglobal.org/spec/lti/claim/ext element must be an object (' . gettype($message->{'https://purl.imsglobal.org/spec/lti/claim/ext'}) . ' found)');
+ }
+ }
+ if (isset($message->{'https://purl.imsglobal.org/spec/lti/claim/custom'})) {
+ if (!is_object($message->{'https://purl.imsglobal.org/spec/lti/claim/custom'})) {
+ Util::setMessage(true,
+ 'The members/message/https://purl.imsglobal.org/spec/lti/claim/custom element must be an object (' . gettype($message->{'https://purl.imsglobal.org/spec/lti/claim/custom'}) . ' found)');
+ }
+ }
+ if (isset($member->group_enrollments)) {
+ if (!is_array($member->group_enrollments)) {
+ if (Util::$strictMode) {
+ Util::setMessage(true,
+ 'The members/message/group_enrollments element must be an array (' . gettype($member->group_enrollments) . ' found)');
+ } else {
+ Util::setMessage(false,
+ 'The members/message/group_enrollments element should be an array (' . gettype($member->group_enrollments) . ' found)');
+ if (is_object($member->group_enrollments)) {
+ $groupenrollments = (array) $member->group_enrollments;
+ }
+ }
+ } else {
+ $groupenrollments = $member->group_enrollments;
+ }
+ }
+ }
+ if (!isset($message->{'https://purl.imsglobal.org/spec/lti/claim/message_type'})) {
+ Util::setMessage(true,
+ 'The members/message elements must include a \'https://purl.imsglobal.org/spec/lti/claim/message_type\' property');
+ } elseif (($message->{'https://purl.imsglobal.org/spec/lti/claim/message_type'} === 'basic-lti-launch-request') ||
+ ($message->{'https://purl.imsglobal.org/spec/lti/claim/message_type'} === 'LtiResourceLinkRequest')) {
if (isset($message->{'https://purl.imsglobal.org/spec/lti-bo/claim/basicoutcome'}) &&
isset($message->{'https://purl.imsglobal.org/spec/lti-bo/claim/basicoutcome'}->lis_result_sourcedid)) {
- $userResult->ltiResultSourcedId = $message->{'https://purl.imsglobal.org/spec/lti-bo/claim/basicoutcome'}->lis_result_sourcedid;
- $doSave = true;
+ $sourcedid = Util::checkString($message->{'https://purl.imsglobal.org/spec/lti-bo/claim/basicoutcome'},
+ 'members/message/https://purl.imsglobal.org/spec/lti-bo/claim/basicoutcome/lis_result_sourcedid');
+ if (empty($userResult->ltiResultSourcedId) || ($userResult->ltiResultSourcedId !== $sourcedid)) {
+ $userResult->ltiResultSourcedId = $sourcedid;
+ $doSave = true;
+ }
} elseif ($userResult->isLearner() && empty($userResult->created)) { // Ensure all learners are recorded in case Assignment and Grade services are used
$userResult->ltiResultSourcedId = '';
$doSave = true;
}
- if (isset($message->{'https://purl.imsglobal.org/spec/lti/claim/ext'})) {
- if (empty($userResult->username)) {
- if (!empty($message->{'https://purl.imsglobal.org/spec/lti/claim/ext'}->username)) {
- $userResult->username = $message->{'https://purl.imsglobal.org/spec/lti/claim/ext'}->username;
- } elseif (!empty($message->{'https://purl.imsglobal.org/spec/lti/claim/ext'}->user_username)) {
- $userResult->username = $message->{'https://purl.imsglobal.org/spec/lti/claim/ext'}->user_username;
- }
+ $username = null;
+ if (isset($message->{'https://purl.imsglobal.org/spec/lti/claim/ext'}) &&
+ is_object($message->{'https://purl.imsglobal.org/spec/lti/claim/ext'})) {
+ if (!empty($message->{'https://purl.imsglobal.org/spec/lti/claim/ext'}->username)) {
+ $username = Util::checkString($message->{'https://purl.imsglobal.org/spec/lti/claim/ext'},
+ 'members/message/https://purl.imsglobal.org/spec/lti/claim/ext/username');
+ } elseif (!empty($message->{'https://purl.imsglobal.org/spec/lti/claim/ext'}->user_username)) {
+ $username = Util::checkString($message->{'https://purl.imsglobal.org/spec/lti/claim/ext'},
+ 'members/message/https://purl.imsglobal.org/spec/lti/claim/ext/user_username');
}
}
- if (isset($message->{'https://purl.imsglobal.org/spec/lti/claim/custom'})) {
- if (empty($userResult->username)) {
- if (!empty($message->{'https://purl.imsglobal.org/spec/lti/claim/custom'}->username)) {
- $userResult->username = $message->{'https://purl.imsglobal.org/spec/lti/claim/custom'}->username;
- } elseif (!empty($message->{'https://purl.imsglobal.org/spec/lti/claim/custom'}->user_username)) {
- $userResult->username = $message->{'https://purl.imsglobal.org/spec/lti/claim/custom'}->user_username;
- }
+ if (empty($username) && isset($message->{'https://purl.imsglobal.org/spec/lti/claim/custom'}) &&
+ is_object($message->{'https://purl.imsglobal.org/spec/lti/claim/custom'})) {
+ if (!empty($message->{'https://purl.imsglobal.org/spec/lti/claim/custom'}->username)) {
+ $username = Util::checkString($message->{'https://purl.imsglobal.org/spec/lti/claim/custom'},
+ 'members/message/https://purl.imsglobal.org/spec/lti/claim/custom/username');
+ } elseif (!empty($message->{'https://purl.imsglobal.org/spec/lti/claim/custom'}->user_username)) {
+ $username = Util::checkString($message->{'https://purl.imsglobal.org/spec/lti/claim/custom'},
+ 'members/message/https://purl.imsglobal.org/spec/lti/claim/custom/user_username');
}
}
+ if (!empty($username)) {
+ $userResult->username = $username;
+ }
break;
}
}
@@ -367,25 +558,36 @@ private function getMembers(bool $withGroups, ?string $role = null, ?int $limit
}
}
$userResults[] = $userResult;
- if (isset($member->group_enrollments)) {
+ if (is_array($groupenrollments)) {
$userResult->groups = [];
- foreach ($member->group_enrollments as $group) {
- $groupId = $group->group_id;
- if (empty($this->source->groups) || !array_key_exists($groupId, $this->source->groups)) {
- $this->source->groups[$groupId] = [
- 'title' => "Group {$groupId}"
- ];
- }
- if (!empty($this->source->groups[$groupId]['set'])) {
- $this->source->groupSets[$this->source->groups[$groupId]['set']]['num_members']++;
- if ($userResult->isStaff()) {
- $this->source->groupSets[$this->source->groups[$groupId]['set']]['num_staff']++;
- }
- if ($userResult->isLearner()) {
- $this->source->groupSets[$this->source->groups[$groupId]['set']]['num_learners']++;
+ foreach ($groupenrollments as $group) {
+ if (!is_object($group)) {
+ Util::setMessage(true,
+ 'The members/group_enrollments element must comprise an array of objects (' . gettype($group) . ' found)');
+ continue;
+ } elseif (!isset($group->group_id)) {
+ Util::setMessage(true, 'The members/group_enrollments objects must have a \'group_id\' property');
+ continue;
+ } else {
+ $groupId = Util::checkString($group, 'members/group_enrollments/group_id');
+ if (!empty($groupId)) {
+ if (empty($this->source->groups) || !array_key_exists($groupId, $this->source->groups)) {
+ $this->source->groups[$groupId] = [
+ 'title' => "Group {$groupId}"
+ ];
+ }
+ if (!empty($this->source->groups[$groupId]['set'])) {
+ $this->source->groupSets[$this->source->groups[$groupId]['set']]['num_members']++;
+ if ($userResult->isStaff()) {
+ $this->source->groupSets[$this->source->groups[$groupId]['set']]['num_staff']++;
+ }
+ if ($userResult->isLearner()) {
+ $this->source->groupSets[$this->source->groups[$groupId]['set']]['num_learners']++;
+ }
+ }
+ $userResult->groups[] = $groupId;
}
}
- $userResult->groups[] = $groupId;
}
}
@@ -402,6 +604,8 @@ private function getMembers(bool $withGroups, ?string $role = null, ?int $limit
$userResult->delete();
}
}
+ } else {
+ $userResults = false;
}
return $userResults;
diff --git a/src/Service/Result.php b/src/Service/Result.php
index b67f302..09fe765 100644
--- a/src/Service/Result.php
+++ b/src/Service/Result.php
@@ -6,6 +6,7 @@
use ceLTIc\LTI\Platform;
use ceLTIc\LTI\User;
use ceLTIc\LTI\Outcome;
+use ceLTIc\LTI\Util;
/**
* Class to implement the Result service
@@ -90,10 +91,21 @@ public function getAll(?int $limit = null): array|bool
do {
$http = $this->send('GET', $params);
$url = '';
+ if ($http->ok) {
+ $http->ok = empty($http->responseJson) || is_array($http->responseJson);
+ }
if ($http->ok) {
if (!empty($http->responseJson)) {
foreach ($http->responseJson as $outcome) {
- $outcomes[] = self::getOutcome($outcome);
+ if (!is_object($outcome)) {
+ $http->ok = false;
+ break;
+ } else {
+ $obj = $this->getOutcome($outcome);
+ if (!is_null($obj)) {
+ $outcomes[] = $obj;
+ }
+ }
}
}
if (!$this->pagingMode && $http->hasRelativeLink('next')) {
@@ -101,11 +113,12 @@ public function getAll(?int $limit = null): array|bool
$this->endpoint = $url;
$params = [];
}
- } else {
- $outcomes = false;
}
} while ($url);
$this->endpoint = $endpoint;
+ if (!$http->ok) {
+ $outcomes = false;
+ }
return $outcomes;
}
@@ -123,9 +136,17 @@ public function get(User $user): Outcome|null|bool
'user_id' => $user->ltiUserId
];
$http = $this->send('GET', $params);
+ if ($http->ok) {
+ $http->ok = empty($http->responseJson) || is_array($http->responseJson);
+ }
if ($http->ok) {
if (!empty($http->responseJson)) {
- $outcome = self::getOutcome(reset($http->responseJson));
+ $obj = reset($http->responseJson);
+ if (is_object($obj)) {
+ $outcome = $this->getOutcome($obj);
+ } else {
+ $outcome = false;
+ }
} else {
$outcome = null;
}
@@ -144,20 +165,29 @@ public function get(User $user): Outcome|null|bool
*
* @param object $json JSON representation of an outcome
*
- * @return Outcome Outcome object
+ * @return Outcome|null Outcome object
*/
- private static function getOutcome(object $json): Outcome
+ private function getOutcome(object $json): ?Outcome
{
- $outcome = new Outcome();
- $outcome->ltiUserId = $json->userId;
- if (isset($json->resultScore)) {
- $outcome->setValue($json->resultScore);
- }
- if (isset($json->resultMaximum)) {
- $outcome->setPointsPossible($json->resultMaximum);
- }
- if (isset($json->comment)) {
- $outcome->comment = $json->comment;
+ $outcome = null;
+ $id = Util::checkString($json, 'id', true);
+ $scoreOf = Util::checkString($json, 'scoreOf', true);
+ $userId = Util::checkString($json, 'userId', true);
+ $resultScore = Util::checkNumber($json, 'resultScore');
+ $resultMaximum = Util::checkNumber($json, 'resultMaximum', false, 0, true);
+ $comment = Util::checkString($json, 'comment');
+ if (!empty($userId)) {
+ $outcome = new Outcome();
+ $outcome->ltiUserId = $userId;
+ if (!is_null($resultScore)) {
+ $outcome->setValue($resultScore);
+ }
+ if (!is_null($resultMaximum)) {
+ $outcome->setPointsPossible($resultMaximum);
+ }
+ if (!empty($comment)) {
+ $outcome->comment = $comment;
+ }
}
return $outcome;
diff --git a/src/Service/ToolSettings.php b/src/Service/ToolSettings.php
index ffa3ca2..85ee072 100644
--- a/src/Service/ToolSettings.php
+++ b/src/Service/ToolSettings.php
@@ -85,21 +85,55 @@ public function __construct(Platform|Context|ResourceLink $source, string $endpo
*/
public function get(?ToolSettingsMode $mode = null): array|bool
{
+ $response = false;
$parameter = [];
if (!empty($mode)) {
$parameter['bubble'] = $mode->value;
}
$http = $this->send('GET', $parameter);
- if (!$http->ok) {
- $response = false;
- } elseif ($this->simple) {
- $response = Util::jsonDecode($http->response, true);
- } elseif (isset($http->responseJson->{'@graph'})) {
- $response = [];
- foreach ($http->responseJson->{'@graph'} as $level) {
- $settings = Util::jsonDecode(json_encode($level->custom), true);
- unset($settings['@id']);
- $response[self::$LEVEL_NAMES[$level->{'@type'}]] = $settings;
+ if ($http->ok) {
+ if ($this->simple) {
+ if (!is_object($http->responseJson)) {
+ Util::setMessage(true, 'The response must be an object');
+ $response = false;
+ } else {
+ $response = Util::jsonDecode(json_encode($http->responseJson), true);
+ if (is_array($response)) {
+ $response = $this->checkSettings($response);
+ } else {
+ Util::setMessage(true, 'The response must be a simple object');
+ $response = false;
+ }
+ }
+ } else {
+ $graph = Util::checkArray($http->responseJson, '@graph', true, true);
+ if (!empty($graph)) {
+ $response = [];
+ foreach ($graph as $level) {
+ $settings = [];
+ if (isset($level->custom)) {
+ if (!is_object($level->custom)) {
+ Util::setMessage(true, 'The custom element must be an object');
+ $response = false;
+ } else {
+ $settings = Util::jsonDecode(json_encode($level->custom), true);
+ if (is_array($settings)) {
+ unset($settings['@id']);
+ $settings = $this->checkSettings($settings);
+ if ($settings === false) {
+ $response = false;
+ }
+ } else {
+ Util::setMessage(true, 'The custom element must be a simple object');
+ $response = false;
+ }
+ }
+ }
+ if ($response !== false) {
+ $response[self::$LEVEL_NAMES[$level->{'@type'}]] = $settings;
+ }
+ }
+ }
}
}
@@ -142,4 +176,38 @@ public function set(array $settings): bool
return $response->ok;
}
+###
+### PRIVATE METHODS
+###
+
+ /**
+ * Check the tool setting values.
+ *
+ * @param array $settings An associative array of settings
+ *
+ * @return array|false Array of settings, or false if an invalid value is found
+ */
+ private function checkSettings(array $settings): array|false
+ {
+ $response = [];
+ foreach ($settings as $key => $value) {
+ if (is_string($value)) {
+ if ($response !== false) {
+ $response[$key] = $value;
+ }
+ } elseif (!Util::$strictMode) {
+ Util::setMessage(false,
+ 'Properties of the custom element should have a string value (' . gettype($value) . ' found)');
+ if ($response !== false) {
+ $response[$key] = Util::valToString($value);
+ }
+ } else {
+ Util::setMessage(true, 'Properties of the custom element must have a string value (' . gettype($value) . ' found)');
+ $response = false;
+ }
+ }
+
+ return $response;
+ }
+
}
diff --git a/src/SubmissionReview.php b/src/SubmissionReview.php
index 0080480..dcdb52f 100644
--- a/src/SubmissionReview.php
+++ b/src/SubmissionReview.php
@@ -3,6 +3,8 @@
namespace ceLTIc\LTI;
+use ceLTIc\LTI\Util;
+
/**
* Class to represent a submission review
*
@@ -53,27 +55,27 @@ public function __construct(?string $label = null, ?string $endpoint = null, ?ar
*
* @param object $json A JSON object representing a submission review
*
- * @return SubmissionReview The SubmissionReview object
+ * @return SubmissionReview|null The SubmissionReview object
*/
- public static function fromJsonObject(object $json): SubmissionReview
+ public static function fromJsonObject(object $json): ?SubmissionReview
{
- if (!empty($json->label)) {
- $label = $json->label;
+ $ok = true;
+ $obj = null;
+ $label = Util::checkString($json, 'SubmissionReview/label', false, true, '', false, null);
+ $ok = $ok && (!is_null($label) || !isset($json->label));
+ $endpoint = Util::checkString($json, 'SubmissionReview/url', false, true, '', false, null);
+ $ok = $ok && (!is_null($endpoint) || !isset($json->url));
+ $custom = Util::checkObject($json, 'SubmissionReview/custom', false, true);
+ if (!is_null($custom)) {
+ $custom = (array) $custom;
} else {
- $label = null;
+ $ok = $ok && !isset($json->custom);
}
- if (!empty($json->url)) {
- $endpoint = $json->url;
- } else {
- $endpoint = null;
- }
- if (!empty($json->custom)) {
- $custom = $json->custom;
- } else {
- $custom = null;
+ if ($ok) {
+ $obj = new SubmissionReview($label, $endpoint, $custom);
}
- return new SubmissionReview($label, $endpoint, $custom);
+ return $obj;
}
/**
diff --git a/src/System.php b/src/System.php
index 5a253f8..ae69282 100644
--- a/src/System.php
+++ b/src/System.php
@@ -122,6 +122,8 @@ trait System
/**
* Warnings relating to last request processed.
*
+ * @deprecated Use Util::getMessages() instead
+ *
* @var array $warnings
*/
public array $warnings = [];
@@ -479,6 +481,7 @@ public function getMessageClaims(bool $fullyQualified = false): ?array
$messageClaims = [];
if (!empty($messageParameters['oauth_consumer_key'])) {
$messageClaims['aud'] = [$messageParameters['oauth_consumer_key']];
+ $messageClaims['azp'] = $messageParameters['oauth_consumer_key'];
}
foreach ($messageParameters as $key => $value) {
$ok = true;
@@ -489,12 +492,17 @@ public function getMessageClaims(bool $fullyQualified = false): ?array
} else {
$mapping = Util::JWT_CLAIM_MAPPING[$key];
}
- if (isset($mapping['isObject']) && $mapping['isObject']) {
- $value = Util::jsonDecode($value);
- } elseif (isset($mapping['isArray']) && $mapping['isArray']) {
+ if (isset($mapping['isArray']) && $mapping['isArray']) {
$value = array_map('trim', explode(',', $value));
$value = array_filter($value);
sort($value);
+ } elseif (isset($mapping['isContentItemSelection']) && $mapping['isContentItemSelection']) {
+ $value = Util::jsonDecode($value);
+ if (is_object($value) && isset($value->{'@graph'}) && is_array($value->{'@graph'})) {
+ $value = $value->{'@graph'};
+ } else if (!is_array($value)) {
+ $value = null;
+ }
} elseif (isset($mapping['isBoolean']) && $mapping['isBoolean']) {
$value = (is_bool($value)) ? $value : $value === 'true';
} elseif (isset($mapping['isInteger']) && $mapping['isInteger']) {
@@ -1075,61 +1083,40 @@ public function getBaseString(): ?string
/**
* Verify the required properties of an LTI message.
*
+ * @param bool $generateWarnings True if warning messages should be generated
+ *
* @return bool True if it is a valid LTI message
*/
- public function checkMessage(): bool
+ public function checkMessage(bool $generateWarnings = false): bool
{
- $ok = $_SERVER['REQUEST_METHOD'] === 'POST';
- if (!$ok) {
- $this->reason = 'LTI messages must use HTTP POST';
- } elseif (!empty($this->jwt) && !empty($this->jwt->hasJwt())) {
- $ok = false;
- $context = $this->jwt->getClaim('https://purl.imsglobal.org/spec/lti/claim/context');
- $resourceLink = $this->jwt->getClaim('https://purl.imsglobal.org/spec/lti/claim/resource_link');
- if (is_null($this->messageParameters['oauth_consumer_key']) || (strlen($this->messageParameters['oauth_consumer_key']) <= 0)) {
- $this->reason = 'Missing iss claim';
- } elseif (empty($this->jwt->getClaim('iat', ''))) {
- $this->reason = 'Missing iat claim';
- } elseif (empty($this->jwt->getClaim('exp', ''))) {
- $this->reason = 'Missing exp claim';
- } elseif (intval($this->jwt->getClaim('iat')) > intval($this->jwt->getClaim('exp'))) {
- $this->reason = 'iat claim must not have a value greater than exp claim';
- } elseif (empty($this->jwt->getClaim('nonce', ''))) {
- $this->reason = 'Missing nonce claim';
- } elseif (!empty($context) && property_exists($context, 'id') && (empty($context->id) || !is_string($context->id))) {
- $this->reason = 'Invalid value for id property in https://purl.imsglobal.org/spec/lti/claim/context claim';
- } elseif (!empty($resourceLink) && property_exists($resourceLink, 'id') && (empty($resourceLink->id) || !is_string($resourceLink->id))) {
- $this->reason = 'Invalid value for id property in https://purl.imsglobal.org/spec/lti/claim/resource_link claim';
- } else {
- $ok = true;
- }
- }
+ $this->ok = $_SERVER['REQUEST_METHOD'] === 'POST';
+ if (!$this->ok) {
+ $this->setReason('LTI messages must use HTTP POST');
+ } else {
// Set signature method from request
- if (isset($this->messageParameters['oauth_signature_method'])) {
- $this->signatureMethod = $this->messageParameters['oauth_signature_method'];
- if (($this instanceof Tool) && !empty($this->platform)) {
- $this->platform->signatureMethod = $this->signatureMethod;
+ if (isset($this->messageParameters['oauth_signature_method'])) {
+ $this->signatureMethod = $this->messageParameters['oauth_signature_method'];
+ if (($this instanceof Tool) && !empty($this->platform)) {
+ $this->platform->signatureMethod = $this->signatureMethod;
+ }
}
- }
// Check all required launch parameters
- if ($ok) {
- $ok = isset($this->messageParameters['lti_message_type']);
- if (!$ok) {
- $this->reason = 'Missing lti_message_type parameter.';
- }
- }
- if ($ok) {
- $ok = isset($this->messageParameters['lti_version']);
- if ($ok) {
- $this->ltiVersion = LtiVersion::tryFrom($this->messageParameters['lti_version']);
- $ok = !empty($this->ltiVersion);
+ if ($this->ok || $generateWarnings) {
+ if (!isset($this->messageParameters['lti_message_type'])) {
+ $this->setReason('Missing \'lti_message_type\' parameter');
+ }
}
- if (!$ok) {
- $this->reason = 'Invalid or missing lti_version parameter.';
+ if ($this->ok || $generateWarnings) {
+ if (isset($this->messageParameters['lti_version'])) {
+ $this->ltiVersion = LtiVersion::tryFrom($this->messageParameters['lti_version']);
+ }
+ if (empty($this->ltiVersion)) {
+ $this->setReason('Invalid or missing \'lti_version\' parameter');
+ }
}
}
- return $ok;
+ return $this->ok;
}
/**
@@ -1139,7 +1126,6 @@ public function checkMessage(): bool
*/
public function verifySignature(): bool
{
- $ok = false;
$key = $this->key;
if (!empty($key)) {
$secret = $this->secret;
@@ -1183,17 +1169,15 @@ public function verifySignature(): bool
$request->unset_parameter('_new_window');
}
$server->verify_request($request);
- $ok = true;
} catch (\Exception $e) {
+ $this->ok = false;
if (empty($this->reason)) {
$oauthConsumer = new OAuth\OAuthConsumer($key, $secret);
$signature = $request->build_signature($method, $oauthConsumer, null);
if ($this->debugMode) {
- $this->reason = $e->getMessage();
- }
- if (empty($this->reason)) {
- $this->reason = 'OAuth signature check failed - perhaps an incorrect secret or timestamp.';
+ $this->setReason($e->getMessage());
}
+ $this->setReason('OAuth signature check failed - perhaps an incorrect secret or timestamp');
$this->details[] = "Shared secret: '{$secret}'";
$this->details[] = 'Current timestamp: ' . time();
$this->details[] = "Expected signature: {$signature}";
@@ -1201,25 +1185,42 @@ public function verifySignature(): bool
}
}
} else { // JWT-signed message
- $nonce = new PlatformNonce($platform, $this->jwt->getClaim('nonce'));
+ $nonce = new PlatformNonce($platform, Util::valToString($this->jwt->getClaim('nonce')));
$ok = !$nonce->load();
if ($ok) {
$ok = $nonce->save();
}
if (!$ok) {
- $this->reason = 'Invalid nonce.';
+ $this->setReason('Invalid nonce');
} elseif (!empty($publicKey) || !empty($jku) || Jwt::$allowJkuHeader) {
$ok = $this->jwt->verify($publicKey, $jku);
if (!$ok) {
- $this->reason = 'JWT signature check failed - perhaps an invalid public key or timestamp';
+ $this->setReason('JWT signature check failed - perhaps an invalid public key or timestamp');
}
} else {
- $ok = false;
- $this->reason = 'Unable to verify JWT signature as neither a public key nor a JSON Web Key URL is specified';
+ $this->setReason('Unable to verify JWT signature as neither a public key nor a JSON Web Key URL is specified');
}
}
- return $ok;
+ return $this->ok;
+ }
+
+ /**
+ * Set the error reason.
+ *
+ * @param string $reason Reason value
+ *
+ * @return bool Returns false
+ */
+ public function setReason(string $reason): bool
+ {
+ $this->ok = false;
+ Util::setMessage(true, $reason);
+ if (empty($this->reason)) {
+ $this->reason = $reason;
+ }
+
+ return false;
}
###
@@ -1229,185 +1230,184 @@ public function verifySignature(): bool
/**
* Parse the message.
*
- * @param bool $strictMode True if full compliance with the LTI specification is required
* @param bool $disableCookieCheck True if no cookie check should be made
* @param bool $generateWarnings True if warning messages should be generated
*
* @return void
*/
- private function parseMessage(bool $strictMode, bool $disableCookieCheck, bool $generateWarnings): void
+ private function parseMessage(bool $disableCookieCheck, bool $generateWarnings): void
{
- if (is_null($this->messageParameters)) {
- $this->getRawParameters();
- if (isset($this->rawParameters['id_token']) || isset($this->rawParameters['JWT'])) { // JWT-signed message
- try {
- $this->jwt = Jwt::getJwtClient();
- if (isset($this->rawParameters['id_token'])) {
- $this->ok = $this->jwt->load($this->rawParameters['id_token'], $this->rsaKey);
- } else {
- $this->ok = $this->jwt->load($this->rawParameters['JWT'], $this->rsaKey);
+ $this->getRawParameters();
+ if (isset($this->rawParameters['id_token']) || isset($this->rawParameters['JWT'])) { // JWT-signed message
+ try {
+ $this->jwt = Jwt::getJwtClient();
+ if (isset($this->rawParameters['id_token'])) {
+ $this->ok = $this->jwt->load($this->rawParameters['id_token'], $this->rsaKey);
+ } else {
+ $this->ok = $this->jwt->load($this->rawParameters['JWT'], $this->rsaKey);
+ }
+ if (!$this->ok) {
+ $this->setReason('Message does not contain a valid JWT');
+ } else {
+ if ($this->ok || $generateWarnings) {
+ $iat = $this->getClaimInteger('iat', true, $generateWarnings);
}
- if (!$this->ok) {
- $this->reason = 'Message does not contain a valid JWT';
- } else {
- $this->ok = $this->jwt->hasClaim('iss') && $this->jwt->hasClaim('aud') && $this->jwt->hasClaim('nonce') &&
- $this->jwt->hasClaim(Util::JWT_CLAIM_PREFIX . '/claim/deployment_id');
- if ($this->ok) {
- $iss = $this->jwt->getClaim('iss');
- $aud = $this->jwt->getClaim('aud');
- $deploymentId = $this->jwt->getClaim(Util::JWT_CLAIM_PREFIX . '/claim/deployment_id');
- $this->ok = !empty($iss) && !empty($aud) && !empty($deploymentId);
- if (!$this->ok) {
- $this->reason = 'iss, aud and/or deployment_id claim is empty';
- } elseif (is_array($aud)) {
- if ($this->jwt->hasClaim('azp')) {
- $this->ok = !empty($this->jwt->getClaim('azp'));
- if (!$this->ok) {
- $this->reason = 'azp claim is empty';
- } else {
- $this->ok = in_array($this->jwt->getClaim('azp'), $aud);
- if ($this->ok) {
- $aud = $this->jwt->getClaim('azp');
- } else {
- $this->reason = 'azp claim value is not included in aud claim';
- }
- }
+ if ($this->ok || $generateWarnings) {
+ $exp = $this->getClaimInteger('exp', true, $generateWarnings);
+ }
+ if (($this->ok || $generateWarnings) && !is_null($iat) && !is_null($exp) && ($iat > $exp)) {
+ $this->setReason('\'iat\' claim must not have a value greater than \'exp\' claim');
+ }
+ if ($this->ok || $generateWarnings) {
+ $nonce = $this->getClaimString('nonce', true, true, $generateWarnings);
+ }
+ if ($this->ok || $generateWarnings) {
+ $iss = $this->getClaimString('iss', true, true, $generateWarnings);
+ }
+ if ($this->ok || $generateWarnings) {
+ $azp = $this->getClaimString('azp', false, true, $generateWarnings);
+ }
+ if ($this->ok || $generateWarnings) {
+ $aud = $this->jwt->getClaim('aud');
+ if (is_string($aud)) {
+ $aud = [$aud];
+ }
+ $aud = $this->checkClaimArray('aud', $aud, true, true, $generateWarnings);
+ if (!empty($aud)) {
+ if (!empty($azp)) {
+ if (in_array($azp, $aud)) {
+ $aud = $azp;
} else {
- $aud = $aud[0];
- $this->ok = !empty($aud);
- if (!$this->ok) {
- $this->reason = 'First element of aud claim is empty';
- }
+ $this->setReason('\'azp\' claim value is not included in \'aud\' claim');
}
- } elseif ($this->jwt->hasClaim('azp')) {
- $this->ok = $this->jwt->getClaim('azp') === $aud;
- if (!$this->ok) {
- $this->reason = 'aud claim does not match the azp claim';
+ } else {
+ $aud = $aud[0];
+ if (empty($aud)) {
+ $this->setReason('First element of \'aud\' claim is empty');
}
}
- if ($this->ok) {
- if ($this instanceof Tool) {
- $this->platform = Platform::fromPlatformId($iss, $aud, $deploymentId, $this->dataConnector);
- $this->platform->platformId = $iss;
- if (isset($this->rawParameters['id_token'])) {
- $this->ok = !empty($this->rawParameters['state']);
- if ($this->ok) {
- $state = $this->rawParameters['state'];
- $parts = explode('.', $state);
- if (!empty(session_id()) && (count($parts) > 1) && (session_id() !== $parts[1]) &&
- ($parts[1] !== 'platformStorage')) { // Reset to original session
- session_abort();
- session_id($parts[1]);
- session_start();
- $this->onResetSessionId();
- }
- $usePlatformStorage = (str_ends_with($state, '.platformStorage'));
- if ($usePlatformStorage) {
- $state = substr($state, 0, -16);
- }
- $this->onAuthenticate($state, $this->jwt->getClaim('nonce'), $usePlatformStorage);
- if (!$disableCookieCheck) {
- if (empty($_COOKIE) && !isset($_POST['_new_window'])) { // Reopen in a new window
- Util::setTestCookie();
- $_POST['_new_window'] = '';
- echo Util::sendForm($_SERVER['REQUEST_URI'], $_POST, '_blank');
- exit;
- }
- Util::setTestCookie(true);
- }
- } else {
- $this->reason = 'state parameter is missing';
- }
- if ($this->ok) {
- $nonce = new PlatformNonce($this->platform, $state);
- $this->ok = $nonce->load();
- if (!$this->ok) {
- $platform = Platform::fromPlatformId($iss, $aud, null, $this->dataConnector);
- $nonce = new PlatformNonce($platform, $state);
- $this->ok = $nonce->load();
- }
- if (!$this->ok) {
- $platform = Platform::fromPlatformId($iss, null, null, $this->dataConnector);
- $nonce = new PlatformNonce($platform, $state);
- $this->ok = $nonce->load();
- }
- if ($this->ok) {
- $this->ok = $nonce->delete();
- }
- if (!$this->ok) {
- $this->reason = 'state parameter is invalid or has expired';
- }
+ }
+ }
+ if ($this->ok || $generateWarnings) {
+ $deploymentId = $this->getClaimString(Util::JWT_CLAIM_PREFIX . '/claim/deployment_id', true, true,
+ $generateWarnings);
+ }
+ if ($this->ok) {
+ if ($this instanceof Tool) {
+ $this->platform = Platform::fromPlatformId($iss, $aud, $deploymentId, $this->dataConnector);
+ $this->platform->platformId = $iss;
+ if (isset($this->rawParameters['id_token'])) {
+ $this->ok = !empty($this->rawParameters['state']);
+ if ($this->ok) {
+ $state = $this->rawParameters['state'];
+ $parts = explode('.', $state);
+ if (!empty(session_id()) && (count($parts) > 1) && (session_id() !== $parts[1]) &&
+ ($parts[1] !== 'platformStorage')) { // Reset to original session
+ session_abort();
+ session_id($parts[1]);
+ session_start();
+ $this->onResetSessionId();
+ }
+ $usePlatformStorage = (str_ends_with($state, '.platformStorage'));
+ if ($usePlatformStorage) {
+ $state = substr($state, 0, -16);
+ }
+ $this->onAuthenticate($state, $nonce, $usePlatformStorage);
+ if (!$disableCookieCheck) {
+ if (empty($_COOKIE) && !isset($_POST['_new_window'])) { // Reopen in a new window
+ Util::setTestCookie();
+ $_POST['_new_window'] = '';
+ echo Util::sendForm($_SERVER['REQUEST_URI'], $_POST, '_blank');
+ exit;
}
+ Util::setTestCookie(true);
}
+ } else {
+ $this->setReason('\'state\' parameter is missing');
}
- $this->messageParameters = [];
if ($this->ok) {
- $this->messageParameters['oauth_consumer_key'] = $aud;
- $this->messageParameters['oauth_signature_method'] = $this->jwt->getHeader('alg');
- $this->parseClaims($strictMode, $generateWarnings);
+ $nonce = new PlatformNonce($this->platform, $state);
+ $this->ok = $nonce->load();
+ if (!$this->ok) {
+ $platform = Platform::fromPlatformId($iss, $aud, null, $this->dataConnector);
+ $nonce = new PlatformNonce($platform, $state);
+ $this->ok = $nonce->load();
+ }
+ if (!$this->ok) {
+ $platform = Platform::fromPlatformId($iss, null, null, $this->dataConnector);
+ $nonce = new PlatformNonce($platform, $state);
+ $this->ok = $nonce->load();
+ }
+ if ($this->ok) {
+ $this->ok = $nonce->delete();
+ }
+ if (!$this->ok) {
+ $this->setReason('\'state\' parameter is invalid or has expired');
+ }
}
}
- } else {
- $this->reason = 'iss, aud, deployment_id and/or nonce claim not found';
+ }
+ $this->messageParameters = [];
+ if ($this->ok) {
+ $this->messageParameters['oauth_consumer_key'] = $aud;
+ $this->messageParameters['oauth_signature_method'] = $this->jwt->getHeader('alg');
+ $this->parseClaims($generateWarnings);
}
}
- } catch (\Exception $e) {
- $this->ok = false;
- $this->reason = 'Message does not contain a valid JWT';
}
- } elseif (isset($this->rawParameters['error'])) { // Error with JWT-signed message
- $this->ok = false;
- $this->reason = $this->rawParameters['error'];
- if (!empty($this->rawParameters['error_description'])) {
- $this->reason .= ": {$this->rawParameters['error_description']}";
+ } catch (\Exception $e) {
+ $this->setReason('Message does not contain a valid JWT');
+ }
+ } elseif (isset($this->rawParameters['error'])) { // Error with JWT-signed message
+ $reason = $this->rawParameters['error'];
+ if (!empty($this->rawParameters['error_description'])) {
+ $reason .= ": {$this->rawParameters['error_description']}";
+ }
+ $this->setReason($reason);
+ } else { // OAuth
+ if ($this instanceof Tool) {
+ if (isset($this->rawParameters['oauth_consumer_key'])) {
+ $this->platform = Platform::fromConsumerKey($this->rawParameters['oauth_consumer_key'], $this->dataConnector);
}
- } else { // OAuth
- if ($this instanceof Tool) {
- if (isset($this->rawParameters['oauth_consumer_key'])) {
- $this->platform = Platform::fromConsumerKey($this->rawParameters['oauth_consumer_key'], $this->dataConnector);
- }
- if (isset($this->rawParameters['tool_state'])) { // Relaunch?
- $state = $this->rawParameters['tool_state'];
- if (!$disableCookieCheck) {
- $parts = explode('.', $state);
- if (empty($_COOKIE) && !isset($_POST['_new_window'])) { // Reopen in a new window
- Util::setTestCookie();
- $_POST['_new_window'] = '';
- echo Util::sendForm($_SERVER['REQUEST_URI'], $_POST, '_blank');
- exit;
- } elseif (!empty(session_id()) && (count($parts) > 1) && (session_id() !== $parts[1])) { // Reset to original session
- session_abort();
- session_id($parts[1]);
- session_start();
- $this->onResetSessionId();
- }
- unset($this->rawParameters['_new_window']);
- Util::setTestCookie(true);
- }
- $nonce = new PlatformNonce($this->platform, $state);
- $this->ok = $nonce->load();
- if (!$this->ok) {
- $this->reason = "Invalid tool_state parameter: '{$state}'";
+ if (isset($this->rawParameters['tool_state'])) { // Relaunch?
+ $state = $this->rawParameters['tool_state'];
+ if (!$disableCookieCheck) {
+ $parts = explode('.', $state);
+ if (empty($_COOKIE) && !isset($_POST['_new_window'])) { // Reopen in a new window
+ Util::setTestCookie();
+ $_POST['_new_window'] = '';
+ echo Util::sendForm($_SERVER['REQUEST_URI'], $_POST, '_blank');
+ exit;
+ } elseif (!empty(session_id()) && (count($parts) > 1) && (session_id() !== $parts[1])) { // Reset to original session
+ session_abort();
+ session_id($parts[1]);
+ session_start();
+ $this->onResetSessionId();
}
+ unset($this->rawParameters['_new_window']);
+ Util::setTestCookie(true);
+ }
+ $nonce = new PlatformNonce($this->platform, $state);
+ $this->ok = $nonce->load();
+ if (!$this->ok) {
+ $this->setReason("Invalid tool_state parameter: '{$state}'");
}
}
- $this->messageParameters = $this->rawParameters;
}
+ $this->messageParameters = $this->rawParameters;
}
}
/**
* Parse the claims
*
- * @param bool $strictMode True if full compliance with the LTI specification is required
* @param bool $generateWarnings True if warning messages should be generated
*
* @return void
*/
- private function parseClaims(bool $strictMode, bool $generateWarnings): void
+ private function parseClaims(bool $generateWarnings): void
{
$payload = Util::cloneObject($this->jwt->getPayload());
- $errors = [];
foreach (Util::JWT_CLAIM_MAPPING as $key => $mapping) {
$claim = Util::JWT_CLAIM_PREFIX;
if (!empty($mapping['suffix'])) {
@@ -1431,34 +1431,39 @@ private function parseClaims(bool $strictMode, bool $generateWarnings): void
if (is_array($group) && array_key_exists($mapping['claim'], $group)) {
unset($payload->{$claim}[$mapping['claim']]);
$value = $group[$mapping['claim']];
+ $claim .= "[{$mapping['claim']}]";
} elseif (is_object($group) && isset($group->{$mapping['claim']})) {
unset($payload->{$claim}->{$mapping['claim']});
$value = $group->{$mapping['claim']};
+ $claim .= "/{$mapping['claim']}";
}
}
if (!is_null($value)) {
if (isset($mapping['isArray']) && $mapping['isArray']) {
if (!is_array($value)) {
- $errors[] = "'{$claim}' claim must be an array";
- } else {
+ $value = $this->checkClaimArray($claim, $value, false, false, $generateWarnings);
+ }
+ if (is_array($value)) {
$value = implode(',', $value);
}
- } elseif (isset($mapping['isObject']) && $mapping['isObject']) {
- $value = json_encode($value);
- } elseif (isset($mapping['isBoolean']) && $mapping['isBoolean']) {
- $value = $value ? 'true' : 'false';
- } elseif (isset($mapping['isInteger']) && $mapping['isInteger']) {
- $value = strval($value);
- } elseif (!is_string($value)) {
- if ($generateWarnings) {
- $this->warnings[] = "Value of claim '{$claim}' is not a string: '{$value}'";
+ } elseif (isset($mapping['isContentItemSelection']) && $mapping['isContentItemSelection']) {
+ $value = $this->checkClaimArray($claim, $value, false);
+ if (is_array($value)) {
+ $value = json_encode($value, JSON_UNESCAPED_SLASHES);
}
- if (!$strictMode) {
- $value = strval($value);
+ } elseif (isset($mapping['isBoolean']) && $mapping['isBoolean']) {
+ $value = $this->checkClaimBoolean($claim, $value, false, $generateWarnings);
+ if (is_bool($value)) {
+ $value = $value ? 'true' : 'false';
}
+ } elseif (isset($mapping['isInteger']) && $mapping['isInteger']) {
+ $value = $this->checkClaimInteger($claim, $value, false, $generateWarnings);
+ $value = Util::valToString($value);
+ } else {
+ $value = $this->checkClaimString($claim, $value, false, false, $generateWarnings);
}
}
- if (!is_null($value) && is_string($value)) {
+ if (is_string($value)) {
$this->messageParameters[$key] = $value;
}
}
@@ -1494,41 +1499,67 @@ private function parseClaims(bool $strictMode, bool $generateWarnings): void
$claim = Util::JWT_CLAIM_PREFIX . '/claim/custom';
if ($this->jwt->hasClaim($claim)) {
unset($payload->{$claim});
- $custom = $this->jwt->getClaim($claim);
- if (!is_array($custom) && !is_object($custom)) {
- $errors[] = "'{$claim}' claim must be an object";
- } else {
+ $custom = $this->getClaimObject($claim, false);
+ if (is_object($custom)) {
foreach ($custom as $key => $value) {
- $this->messageParameters["custom_{$key}"] = $value;
+ if (!is_string($value)) {
+ if (Util::$strictMode) {
+ $this->setReason("Properties of the '{$claim}' claim object must have string values (" . gettype($value) . ' found)');
+ } else {
+ Util::setMessage(false,
+ "Properties of the '{$claim}' claim object should have string values (" . gettype($value) . ' found)');
+ $value = Util::valToString($value);
+ }
+ }
+ if (is_string($value)) {
+ $this->messageParameters["custom_{$key}"] = $value;
+ }
}
}
}
$claim = Util::JWT_CLAIM_PREFIX . '/claim/ext';
if ($this->jwt->hasClaim($claim)) {
unset($payload->{$claim});
- $ext = $this->jwt->getClaim($claim);
- if (!is_array($ext) && !is_object($ext)) {
- $errors[] = "'{$claim}' claim must be an object";
- } else {
+ $ext = $this->getClaimObject($claim, false);
+ if (is_object($ext)) {
foreach ($ext as $key => $value) {
- $this->messageParameters["ext_{$key}"] = $value;
+ if (!is_string($value)) {
+ if (Util::$strictMode) {
+ $this->setReason("Properties of the '{$claim}' claim object must have string values (" . gettype($value) . ' found)');
+ } else {
+ Util::setMessage(false,
+ "Properties of the '{$claim}' claim object should have string values (" . gettype($value) . ' found)');
+ $value = Util::valToString($value);
+ }
+ }
+ if (is_string($value)) {
+ $this->messageParameters["ext_{$key}"] = $value;
+ }
}
}
}
$claim = Util::JWT_CLAIM_PREFIX . '/claim/lti1p1';
if ($this->jwt->hasClaim($claim)) {
unset($payload->{$claim});
- $lti1p1 = $this->jwt->getClaim($claim);
- if (!is_array($lti1p1) && !is_object($lti1p1)) {
- $errors[] = "'{$claim}' claim must be an object";
- } else {
+ $lti1p1 = $this->getClaimObject($claim, false);
+ if (is_array($lti1p1)) {
foreach ($lti1p1 as $key => $value) {
if (is_null($value)) {
$value = '';
} elseif (is_object($value)) {
$value = json_encode($value);
+ } elseif (!is_string($value)) {
+ if (Util::$strictMode) {
+ $this->setReason("Properties of the '{$claim}' claim object must have string or object values (" . gettype($value) . ' found)');
+ } else {
+ Util::setMessage(false,
+ "Properties of the '{$claim}' claim object should have string or object values (" . gettype($value) . ' found)');
+ $value = Util::valToString($value);
+ }
+ }
+ if (is_string($value)) {
+ $this->messageParameters["lti1p1_{$key}"] = $value;
}
- $this->messageParameters["lti1p1_{$key}"] = $value;
}
}
}
@@ -1537,12 +1568,12 @@ private function parseClaims(bool $strictMode, bool $generateWarnings): void
$d2l = $this->jwt->getClaim($claim);
if (is_array($d2l)) {
if (!empty($d2l['username'])) {
- $this->messageParameters['ext_d2l_username'] = $d2l['username'];
+ $this->messageParameters['ext_d2l_username'] = Util::valToString($d2l['username']);
unset($payload->{$claim}['username']);
}
} elseif (isset($ext) && is_object($ext)) {
if (!empty($d2l->username)) {
- $this->messageParameters['ext_d2l_username'] = $d2l->username;
+ $this->messageParameters['ext_d2l_username'] = Util::valToString($d2l->username);
unset($payload->{$claim}->username);
}
}
@@ -1556,10 +1587,6 @@ private function parseClaims(bool $strictMode, bool $generateWarnings): void
}
$this->messageParameters['unmapped_claims'] = json_encode($payload);
}
- if (!empty($errors)) {
- $this->ok = false;
- $this->reason = 'Invalid JWT: ' . implode(', ', $errors);
- }
}
/**
@@ -1579,8 +1606,7 @@ private function doCallback(): void
if (method_exists($this, $callback)) {
$this->$callback();
} elseif ($this->ok) {
- $this->ok = false;
- $this->reason = "Message type not supported: {$this->messageParameters['lti_message_type']}";
+ $this->setReason("Message type not supported: {$this->messageParameters['lti_message_type']}");
}
}
@@ -1862,4 +1888,256 @@ private static function fullyQualifyClaim(string $claim, mixed $value): array
return $claims;
}
+ /**
+ * Check a JWT claim value is a string.
+ *
+ * @param string $name Name of claim
+ * @param mixed $value Value to check
+ * @param bool $required True if the claim must be present (optional, default is false)
+ * @param bool $notEmpty True if the claim must not be empty (optional, default is false)
+ * @param bool $generateWarnings True if warning messages should be generated (optional, default is false)
+ *
+ * @return string|null String value (or null if not valid)
+ */
+ private function checkClaimString(string $name, mixed $value, bool $required = false, bool $notEmpty = false,
+ bool $generateWarnings = false): ?string
+ {
+ if (!is_null($value)) {
+ $type = gettype($value);
+ if (!is_string($value) && !Util::$strictMode) {
+ if ($generateWarnings) {
+ $this->warnings[] = "Value of claim '{$name}' is not a string: '{$value}'";
+ Util::setMessage(false, "The '{$name}' claim should have a string value ({$type} found)");
+ }
+ $value = Util::valToString($value, null);
+ }
+ if (!is_string($value)) {
+ $this->setReason("'{$name}' claim must have a string value ({$type} found)");
+ $value = null;
+ } else {
+ $value = trim($value);
+ if ($notEmpty && empty($value)) {
+ $this->setReason("'{$name}' claim must not be empty");
+ $value = null;
+ }
+ }
+ } elseif ($required) {
+ $this->setReason("Missing '{$name}' claim");
+ }
+
+ return $value;
+ }
+
+ /**
+ * Get the named string claim from JWT.
+ *
+ * @param string $name Name of claim
+ * @param bool $required True if the claim must be present (optional, default is false)
+ * @param bool $notEmpty True if the claim must not be empty (optional, default is false)
+ * @param bool $generateWarnings True if warning messages should be generated (optional, default is false)
+ *
+ * @return string|null Value of element (or null if not found or valid)
+ */
+ private function getClaimString(string $name, bool $required = false, bool $notEmpty = false, bool $generateWarnings = false): ?string
+ {
+ $value = $this->jwt->getClaim($name);
+
+ return $this->checkClaimString($name, $value, $required, $notEmpty, $generateWarnings);
+ }
+
+ /**
+ * Check a JWT claim value is an integer.
+ *
+ * @param string $name Name of claim
+ * @param mixed $value Value to check
+ * @param bool $required True if the claim must be present (optional, default is false)
+ * @param bool $generateWarnings True if warning messages should be generated (optional, default is false)
+ *
+ * @return int|null Value of element (or null if not found or valid)
+ */
+ private function checkClaimInteger(string $name, mixed $value, bool $required = false, bool $generateWarnings = false): ?int
+ {
+ if (!is_null($value)) {
+ $type = gettype($value);
+ if (!is_int($value) && !Util::$strictMode) {
+ if ($generateWarnings) {
+ Util::setMessage(false, "The '{$name}' claim should have an integer value ({$type} found)");
+ }
+ $value = Util::valToNumber($value);
+ if (is_float($value)) {
+ $value = intval($value);
+ }
+ }
+ if (!is_int($value)) {
+ $this->setReason("'{$name}' claim must have an integer value ({$type} found)");
+ $value = null;
+ }
+ } elseif ($required) {
+ $this->setReason("Missing '{$name}' claim");
+ }
+
+ return $value;
+ }
+
+ /**
+ * Get the named integer claim from JWT.
+ *
+ * @param string $name Name of claim
+ * @param bool $required True if the claim must be present (optional, default is false)
+ * @param bool $notEmpty True if the claim must not be empty (optional, default is false)
+ * @param bool $generateWarnings True if warning messages should be generated (optional, default is false)
+ *
+ * @return int|null Value of element (or null if not found or valid)
+ */
+ private function getClaimInteger(string $name, bool $required = false, bool $generateWarnings = false): ?int
+ {
+ $value = $this->jwt->getClaim($name);
+
+ return $this->checkClaimInteger($name, $value, $required, $generateWarnings);
+ }
+
+ /**
+ * Check a JWT claim value is a boolean.
+ *
+ * @param string $name Name of claim
+ * @param mixed $value Value to check
+ * @param bool $required True if the claim must be present (optional, default is false)
+ * @param bool $generateWarnings True if warning messages should be generated (optional, default is false)
+ *
+ * @return bool|null Value of element (or null if not found or valid)
+ */
+ private function checkClaimBoolean(string $name, mixed $value, bool $required = false, bool $generateWarnings = false): ?bool
+ {
+ if (!is_null($value)) {
+ $type = gettype($value);
+ if (!is_bool($value) && !Util::$strictMode) {
+ if ($generateWarnings) {
+ Util::setMessage(false, "The '{$name}' claim should have a boolean value ({$type} found)");
+ }
+ $value = Util::valToBoolean($value);
+ }
+ if (!is_bool($value)) {
+ $this->setReason("'{$name}' claim must have a boolean value ({$type} found)");
+ $value = null;
+ }
+ } elseif ($required) {
+ $this->setReason("Missing '{$name}' claim");
+ }
+
+ return $value;
+ }
+
+ /**
+ * Get the named boolean claim from JWT.
+ *
+ * @param string $name Name of claim
+ * @param bool $required True if the claim must be present (optional, default is false)
+ * @param bool $notEmpty True if the claim must not be empty (optional, default is false)
+ * @param bool $generateWarnings True if warning messages should be generated (optional, default is false)
+ *
+ * @return bool|null Value of element (or null if not found or valid)
+ */
+ private function getClaimBoolean(string $name, bool $required = false, bool $generateWarnings = false): ?bool
+ {
+ $value = $this->jwt->getClaim($name);
+
+ return $this->checkClaimBoolean($name, $value, $required, $generateWarnings);
+ }
+
+ /**
+ * Check a JWT claim value is an integer.
+ *
+ * @param string $name Name of claim
+ * @param mixed $value Value to check
+ * @param bool $required True if the claim must be present (optional, default is false)
+ * @param bool $notEmpty True if the claim must not be empty (optional, default is false)
+ * @param bool $generateWarnings True if warning messages should be generated (optional, default is false)
+ *
+ * @return array|null Value of element (or null if not found or valid)
+ */
+ private function checkClaimArray(string $name, mixed $value, bool $required = false, bool $notEmpty = false,
+ bool $generateWarnings = false): ?array
+ {
+ if (!is_null($value)) {
+ $type = gettype($value);
+ if (!is_array($value) && !Util::$strictMode) {
+ if ($generateWarnings) {
+ Util::setMessage(false, "The '{$name}' claim should have an array value ({$type} found)");
+ }
+ $value = Util::valToString($value, null);
+ if (is_string($value)) {
+ $value = [$value];
+ } else {
+ $value = null;
+ }
+ }
+ if (!is_array($value)) {
+ $this->setReason("'{$name}' claim must have an array value ({$type} found)");
+ $value = null;
+ } elseif ($notEmpty && empty($value)) {
+ $this->setReason("'{$name}' claim must not be empty");
+ $value = null;
+ }
+ } elseif ($required) {
+ $this->setReason("Missing '{$name}' claim");
+ }
+
+ return $value;
+ }
+
+ /**
+ * Get the named array claim from JWT.
+ *
+ * @param string $name Name of claim
+ * @param bool $required True if the claim must be present (optional, default is false)
+ * @param bool $notEmpty True if the claim must not be empty (optional, default is false)
+ * @param bool $generateWarnings True if warning messages should be generated (optional, default is false)
+ *
+ * @return array|null Value of element (or null if not found or valid)
+ */
+ private function getClaimArray(string $name, bool $required = false, bool $notEmpty = false, bool $generateWarnings = false): ?array
+ {
+ $value = $this->jwt->getClaim($name);
+
+ return $this->checkClaimArray($name, $value, $required, $notEmpty, $generateWarnings);
+ }
+
+ /**
+ * Check a JWT claim value is an object.
+ *
+ * @param string $name Name of claim
+ * @param mixed $value Value to check
+ * @param bool $required True if the claim must be present (optional, default is false)
+ *
+ * @return object|null Value of element (or null if not found or valid)
+ */
+ private function checkClaimObject(string $name, mixed $value, bool $required = false): ?object
+ {
+ if (!is_null($value)) {
+ if (!is_object($value)) {
+ $this->setReason("'{$name}' claim must be an object (" . gettype($value) . ' found)');
+ $value = null;
+ }
+ } elseif ($required) {
+ $this->setReason("Missing '{$name}' claim");
+ }
+
+ return $value;
+ }
+
+ /**
+ * Get the named object claim from JWT.
+ *
+ * @param string $name Name of claim
+ * @param bool $required True if the claim must be present (optional, default is false)
+ *
+ * @return object|null Value of element (or null if not found or valid)
+ */
+ private function getClaimObject(string $name, bool $required = false): ?object
+ {
+ $value = $this->jwt->getClaim($name);
+
+ return $this->checkClaimObject($name, $value, $required);
+ }
+
}
diff --git a/src/Tool.php b/src/Tool.php
index 11e3dfc..5bd8a8c 100644
--- a/src/Tool.php
+++ b/src/Tool.php
@@ -405,16 +405,22 @@ public function delete(): bool
/**
* Get the message parameters.
*
- * @param bool $strictMode True if full compliance with the LTI specification is required (optional, default is false)
+ * @param bool|null $strictMode True if full compliance with the LTI specification is required (optional, default is the Util::$strictMode setting)
* @param bool $disableCookieCheck True if no cookie check should be made (optional, default is false)
* @param bool $generateWarnings True if warning messages should be generated (optional, default is false)
*
* @return array|null The message parameter array
*/
- public function getMessageParameters(bool $strictMode = false, bool $disableCookieCheck = false, bool $generateWarnings = false): ?array
+ public function getMessageParameters(?bool $strictMode = null, bool $disableCookieCheck = false, bool $generateWarnings = false): ?array
{
- if (is_null($this->messageParameters)) {
- $this->parseMessage($strictMode, $disableCookieCheck, $generateWarnings);
+ $currentStrictMode = Util::$strictMode;
+ if (!is_null($strictMode)) {
+ Util::$strictMode = $strictMode;
+ }
+ if ($this->ok && is_null($this->messageParameters)) {
+ $this->parseMessage($disableCookieCheck, $generateWarnings);
+ }
+ if ($this->ok && is_null($this->messageParameters)) {
// Set debug mode
if (!Util::$logLevel->logDebug()) {
$this->debugMode = (isset($this->messageParameters['custom_debug']) &&
@@ -433,6 +439,7 @@ public function getMessageParameters(bool $strictMode = false, bool $disableCook
$this->returnUrl = $this->messageParameters['launch_presentation_return_url'];
}
}
+ Util::$strictMode = $currentStrictMode;
return $this->messageParameters;
}
@@ -440,12 +447,16 @@ public function getMessageParameters(bool $strictMode = false, bool $disableCook
/**
* Process an incoming request
*
- * @param bool $strictMode True if full compliance with the LTI specification is required (optional, default is false)
+ * @param bool|null $strictMode True if full compliance with the LTI specification is required (optional, default is the Util::$strictMode setting)
* @param bool $disableCookieCheck True if no cookie check should be made (optional, default is false)
* @param bool $generateWarnings True if warning messages should be generated (optional, default is false)
*/
- public function handleRequest(bool $strictMode = false, bool $disableCookieCheck = false, bool $generateWarnings = false): void
+ public function handleRequest(bool $strictMode = null, bool $disableCookieCheck = false, bool $generateWarnings = false): void
{
+ $currentStrictMode = Util::$strictMode;
+ if (!is_null($strictMode)) {
+ Util::$strictMode = $strictMode;
+ }
$parameters = Util::getRequestParameters();
if ($this->debugMode) {
Util::$logLevel = LogLevel::Debug;
@@ -455,26 +466,26 @@ public function handleRequest(bool $strictMode = false, bool $disableCookieCheck
} elseif (isset($parameters['iss']) && (strlen($parameters['iss']) > 0)) { // Initiate login request
Util::logRequest();
if (!isset($parameters['login_hint']) || (strlen($parameters['login_hint']) <= 0)) {
- $this->ok = false;
- $this->reason = 'Missing login_hint parameter.';
+ $this->setReason('Missing \'login_hint\' parameter');
} elseif (!isset($parameters['target_link_uri']) || (strlen($parameters['target_link_uri']) <= 0)) {
- $this->ok = false;
- $this->reason = 'Missing target_link_uri parameter.';
+ $this->setReason('Missing \'target_link_uri\' parameter');
} else {
+ Util::$strictMode = $currentStrictMode;
$this->ok = $this->sendAuthenticationRequest($parameters, $disableCookieCheck);
}
} elseif (isset($parameters['openid_configuration']) && (strlen($parameters['openid_configuration']) > 0)) { // Dynamic registration request
Util::logRequest();
$this->onRegistration();
} else { // LTI message
- $this->getMessageParameters($strictMode, $disableCookieCheck, $generateWarnings);
Util::logRequest();
- if ($this->ok && $this->authenticate($strictMode, $disableCookieCheck, $generateWarnings)) {
- if (empty($this->output)) {
- $this->doCallback();
- if ($this->ok && ($this->messageParameters['lti_message_type'] === 'ToolProxyRegistrationRequest')) {
- $this->platform->save();
- }
+ $this->getMessageParameters($strictMode, $disableCookieCheck, $generateWarnings);
+ if (($this->ok || $generateWarnings) && !is_null($this->messageParameters)) {
+ $this->authenticate($disableCookieCheck, $generateWarnings);
+ }
+ if ($this->ok && empty($this->output)) {
+ $this->doCallback();
+ if ($this->ok && ($this->messageParameters['lti_message_type'] === 'ToolProxyRegistrationRequest')) {
+ $this->platform->save();
}
}
}
@@ -488,6 +499,7 @@ public function handleRequest(bool $strictMode = false, bool $disableCookieCheck
}
Util::logError($errorMessage);
}
+ Util::$strictMode = $currentStrictMode;
$this->result();
}
@@ -595,7 +607,7 @@ public function doToolProxyService(): bool
*/
protected function onLaunch(): void
{
- $this->reason = 'No onLaunch method found for tool.';
+ $this->setReason('No onLaunch method found for tool');
$this->onError();
}
@@ -606,7 +618,7 @@ protected function onLaunch(): void
*/
protected function onConfigure(): void
{
- $this->reason = 'No onConfigure method found for tool.';
+ $this->setReason('No onConfigure method found for tool');
$this->onError();
}
@@ -617,7 +629,7 @@ protected function onConfigure(): void
*/
protected function onDashboard(): void
{
- $this->reason = 'No onDashboard method found for tool.';
+ $this->setReason('No onDashboard method found for tool');
$this->onError();
}
@@ -628,7 +640,7 @@ protected function onDashboard(): void
*/
protected function onContentItem(): void
{
- $this->reason = 'No onContentItem method found for tool.';
+ $this->setReason('No onContentItem method found for tool');
$this->onError();
}
@@ -639,7 +651,7 @@ protected function onContentItem(): void
*/
protected function onContentItemUpdate(): void
{
- $this->reason = 'No onContentItemUpdate method found for tool.';
+ $this->setReason('No onContentItemUpdate method found for tool');
$this->onError();
}
@@ -650,7 +662,7 @@ protected function onContentItemUpdate(): void
*/
protected function onSubmissionReview(): void
{
- $this->reason = 'No onSubmissionReview method found for tool.';
+ $this->setReason('No onSubmissionReview method found for tool');
$this->onError();
}
@@ -678,7 +690,7 @@ protected function onRegistration(): void
*/
protected function onLtiStartProctoring(): void
{
- $this->reason = 'No onLtiStartProctoring method found for tool.';
+ $this->setReason('No onLtiStartProctoring method found for tool');
$this->onError();
}
@@ -689,7 +701,7 @@ protected function onLtiStartProctoring(): void
*/
protected function onLtiEndAssessment(): void
{
- $this->reason = 'No onLtiEndAssessment method found for tool.';
+ $this->setReason('No onLtiEndAssessment method found for tool');
$this->onError();
}
@@ -742,12 +754,10 @@ protected function onAuthenticate(string $state, string $nonce, bool $usePlatfor
$state = $parts[0];
$parts = explode('.', $this->rawParameters['_storage_check']);
if ((count($parts) !== 2) || ($parts[0] !== $state) || ($parts[1] !== $nonce)) {
- $this->ok = false;
- $this->reason = 'Invalid state and/or nonce values';
+ $this->setReason('Invalid \'state\' and/or \'nonce\' values');
}
} else {
- $this->ok = false;
- $this->reason = 'Error accessing platform storage';
+ $this->setReason('Error accessing platform storage');
}
} elseif (isset($_SESSION['ceLTIc_lti_authentication_request'])) {
$auth = $_SESSION['ceLTIc_lti_authentication_request'];
@@ -755,8 +765,7 @@ protected function onAuthenticate(string $state, string $nonce, bool $usePlatfor
$state = substr($state, 0, -16);
}
if (($state !== $auth['state']) || ($nonce !== $auth['nonce'])) {
- $this->ok = false;
- $this->reason = 'Invalid state parameter value and/or nonce claim value';
+ $this->setReason('Invalid \'state\' parameter value and/or \'nonce\' claim value');
}
unset($_SESSION['ceLTIc_lti_authentication_request']);
}
@@ -803,10 +812,10 @@ protected function getPlatformConfiguration(): ?array
$this->ok = !empty($platformConfig);
}
if (!$this->ok) {
- $this->reason = 'Unable to access platform configuration details.';
+ $this->setReason('Unable to access platform configuration details');
}
} else {
- $this->reason = 'Invalid registration request: missing openid_configuration parameter.';
+ $this->setReason('Invalid registration request: missing \'openid_configuration\ parameter');
}
if ($this->ok) {
$this->ok = !empty($platformConfig['registration_endpoint']) && !empty($platformConfig['jwks_uri']) && !empty($platformConfig['authorization_endpoint']) &&
@@ -817,7 +826,7 @@ protected function getPlatformConfiguration(): ?array
!empty($platformConfig['https://purl.imsglobal.org/spec/lti-platform-configuration']['version']) &&
!empty($platformConfig['https://purl.imsglobal.org/spec/lti-platform-configuration']['messages_supported']);
if (!$this->ok) {
- $this->reason = 'Invalid platform configuration details.';
+ $this->setReason('Invalid platform configuration details');
}
}
if ($this->ok) {
@@ -828,7 +837,7 @@ protected function getPlatformConfiguration(): ?array
if ($this->ok) {
rsort($platformConfig['id_token_signing_alg_values_supported']);
} else {
- $this->reason = 'None of the signature algorithms offered by the platform is supported.';
+ $this->setReason('None of the signature algorithms offered by the platform is supported');
}
}
}
@@ -976,7 +985,7 @@ protected function sendRegistration(array $platformConfig, array $toolConfig): ?
$this->ok = !empty($registrationConfig);
}
if (!$this->ok) {
- $this->reason = 'Unable to register with platform.';
+ $this->setReason('Unable to register with platform');
}
}
if (!$this->ok) {
@@ -1019,7 +1028,7 @@ protected function getPlatformToRegister(array $platformConfig, array $registrat
if ($doSave) {
$this->ok = $this->platform->save();
if (!$this->ok) {
- $this->reason = 'Sorry, an error occurred when saving the platform details.';
+ $this->setReason('Sorry, an error occurred when saving the platform details');
}
}
@@ -1278,42 +1287,45 @@ private function result(): void
*
* The platform, resource link and user objects will be initialised if the request is valid.
*
- * @param bool $strictMode True if full compliance with the LTI specification is required
* @param bool $disableCookieCheck True if no cookie check should be made
* @param bool $generateWarnings True if warning messages should be generated
*
- * @return bool True if the request has been successfully validated.
+ * @return void
*/
- private function authenticate(bool $strictMode, bool $disableCookieCheck, bool $generateWarnings): bool
+ private function authenticate(bool $disableCookieCheck, bool $generateWarnings): void
{
$doSavePlatform = false;
- $this->ok = $this->checkMessage();
- if (($this->ok || $generateWarnings) && !empty($this->jwt) && !empty($this->jwt->hasJwt())) {
- if ($this->jwt->hasClaim('sub') && (strlen($this->jwt->getClaim('sub')) <= 0)) {
- $this->setError('Empty sub claim', $strictMode, $generateWarnings);
+ $this->checkMessage($generateWarnings);
+ if (!empty($this->jwt) && !empty($this->jwt->hasJwt())) {
+ if ($this->ok || $generateWarnings) {
+ $sub = $this->getClaimString('sub', true, true, $generateWarnings);
}
- if (!empty($this->jwt->getClaim('https://purl.imsglobal.org/spec/lti/claim/context', '')) &&
- empty($this->messageParameters['context_id'])) {
- $this->setError('Missing id property in https://purl.imsglobal.org/spec/lti/claim/context claim', $strictMode,
- $generateWarnings);
- } elseif (!empty($this->jwt->getClaim('https://purl.imsglobal.org/spec/lti/claim/tool_platform', '')) &&
- empty($this->messageParameters['tool_consumer_instance_guid'])) {
- $this->setError('Missing guid property in https://purl.imsglobal.org/spec/lti/claim/tool_platform claim',
- $strictMode, $generateWarnings);
+ if ($this->ok || $generateWarnings) {
+ if (!empty($this->jwt->getClaim('https://purl.imsglobal.org/spec/lti/claim/context', '')) &&
+ empty($this->messageParameters['context_id'])) {
+ $this->setError('Missing \'id\' property in \'https://purl.imsglobal.org/spec/lti/claim/context\' claim',
+ Util::$strictMode, $generateWarnings);
+ }
+ }
+ if ($this->ok || $generateWarnings) {
+ if (!empty($this->jwt->getClaim('https://purl.imsglobal.org/spec/lti/claim/tool_platform', '')) &&
+ empty($this->messageParameters['tool_consumer_instance_guid'])) {
+ $this->setError('Missing \'guid\' property in \'https://purl.imsglobal.org/spec/lti/claim/tool_platform\' claim',
+ Util::$strictMode, $generateWarnings);
+ }
}
}
if (($this->ok || $generateWarnings) && !empty($this->messageParameters['lti_message_type'])) {
if ($this->messageParameters['lti_message_type'] === 'basic-lti-launch-request') {
if ($this->ok && (!isset($this->messageParameters['resource_link_id']) || (strlen(trim($this->messageParameters['resource_link_id'])) <= 0))) {
- $this->ok = false;
- $this->reason = 'Missing resource link ID.';
+ $this->setReason('Missing resource link ID', true, $generateWarnings);
}
if ($this->ltiVersion === LtiVersion::V1P3) {
if (!isset($this->messageParameters['roles'])) {
- $this->setError('Missing roles parameter.', $strictMode, $generateWarnings);
+ $this->setError('Missing roles parameter', Util::$strictMode, $generateWarnings);
} elseif (!empty($this->messageParameters['roles']) && empty(array_intersect(self::parseRoles($this->messageParameters['roles'],
LtiVersion::V1P3), User::PRINCIPAL_ROLES))) {
- $this->setError('No principal role found in roles parameter.', $strictMode, $generateWarnings);
+ $this->setError('No principal role found in roles parameter', Util::$strictMode, $generateWarnings);
}
}
} elseif (($this->messageParameters['lti_message_type'] === 'ContentItemSelectionRequest') ||
@@ -1329,18 +1341,18 @@ private function authenticate(bool $strictMode, bool $disableCookieCheck, bool $
$mediaTypes = array_unique($mediaTypes);
}
if ((count($mediaTypes) <= 0) && ($this->ltiVersion === LtiVersion::V1P3)) {
- $this->setError('Missing or empty accept_media_types parameter.', $strictMode, $generateWarnings);
+ $this->setError('Missing or empty accept_media_types parameter', Util::$strictMode, $generateWarnings);
}
if ($isUpdate) {
if ($this->ltiVersion === LtiVersion::V1P3) {
if (!$this->checkValue($this->messageParameters['accept_media_types'],
[Item::LTI_LINK_MEDIA_TYPE, Item::LTI_ASSIGNMENT_MEDIA_TYPE],
- 'Invalid value in accept_media_types parameter: \'%s\'.', $strictMode, $generateWarnings, true)) {
+ 'Invalid value in accept_media_types parameter: \'%s\'', $generateWarnings, true)) {
$this->ok = false;
}
} elseif (!$this->checkValue($this->messageParameters['accept_types'],
- [Item::TYPE_LTI_LINK, Item::TYPE_LTI_ASSIGNMENT], 'Invalid value in accept_types parameter: \'%s\'.',
- $strictMode, $generateWarnings, true)) {
+ [Item::TYPE_LTI_LINK, Item::TYPE_LTI_ASSIGNMENT], 'Invalid value in accept_types parameter: \'%s\'',
+ $generateWarnings, true)) {
$this->ok = false;
}
}
@@ -1372,7 +1384,7 @@ private function authenticate(bool $strictMode, bool $disableCookieCheck, bool $
$documentTargets = array_filter($documentTargets);
$documentTargets = array_unique($documentTargets);
if (count($documentTargets) <= 0) {
- $this->setError('Missing or empty accept_presentation_document_targets parameter.', $strictMode,
+ $this->setError('Missing or empty accept_presentation_document_targets parameter', Util::$strictMode,
$generateWarnings);
} elseif (!empty($documentTargets)) {
if (empty($this->jwt) || !$this->jwt->hasJwt()) {
@@ -1382,18 +1394,18 @@ private function authenticate(bool $strictMode, bool $disableCookieCheck, bool $
}
foreach ($documentTargets as $documentTarget) {
if (!$this->checkValue($documentTarget, $permittedTargets,
- 'Invalid value in accept_presentation_document_targets parameter: \'%s\'.', $strictMode,
- $generateWarnings, true)) {
+ 'Invalid value in accept_presentation_document_targets parameter: \'%s\'', $generateWarnings,
+ true)) {
$this->ok = false;
}
}
}
} else {
- $this->setError('No accept_presentation_document_targets parameter found.', $strictMode, $generateWarnings);
+ $this->setError('No accept_presentation_document_targets parameter found', Util::$strictMode, $generateWarnings);
}
if ($this->ok || $generateWarnings) {
if (empty($this->messageParameters['content_item_return_url'])) {
- $this->setError('Missing content_item_return_url parameter.', true, $generateWarnings);
+ $this->setError('Missing content_item_return_url parameter', true, $generateWarnings);
}
}
if ($this->ok) {
@@ -1404,19 +1416,19 @@ private function authenticate(bool $strictMode, bool $disableCookieCheck, bool $
}
} elseif ($this->messageParameters['lti_message_type'] === 'LtiSubmissionReviewRequest') {
if (!isset($this->messageParameters['custom_lineitem_url']) || (strlen(trim($this->messageParameters['custom_lineitem_url'])) <= 0)) {
- $this->setError('Missing LineItem service URL.', true, $generateWarnings);
+ $this->setError('Missing LineItem service URL', true, $generateWarnings);
}
if (!isset($this->messageParameters['for_user_id']) || (strlen(trim($this->messageParameters['for_user_id'])) <= 0)) {
$this->setError('Missing ID of \'for user\'', true, $generateWarnings);
}
if (($this->ok || $generateWarnings) && ($this->ltiVersion === LtiVersion::V1P3)) {
if (!isset($this->messageParameters['roles'])) {
- $this->setError('Missing roles parameter.', $strictMode, $generateWarnings);
+ $this->setError('Missing roles parameter', Util::$strictMode, $generateWarnings);
}
}
} elseif ($this->messageParameters['lti_message_type'] === 'ToolProxyRegistrationRequest') {
if (!method_exists($this, 'onRegister')) {
- $this->setError('No onRegister method found for tool.', true, $generateWarnings);
+ $this->setError('No onRegister method found for tool', true, $generateWarnings);
} elseif ((!isset($this->messageParameters['reg_key']) ||
(strlen(trim($this->messageParameters['reg_key'])) <= 0)) ||
(!isset($this->messageParameters['reg_password']) ||
@@ -1425,18 +1437,18 @@ private function authenticate(bool $strictMode, bool $disableCookieCheck, bool $
(strlen(trim($this->messageParameters['tc_profile_url'])) <= 0)) ||
(!isset($this->messageParameters['launch_presentation_return_url']) ||
(strlen(trim($this->messageParameters['launch_presentation_return_url'])) <= 0))) {
- $this->setError('Missing message parameters.', true, $generateWarnings);
+ $this->setError('Missing message parameters', true, $generateWarnings);
}
} elseif ($this->messageParameters['lti_message_type'] === 'LtiStartProctoring') {
if (!isset($this->messageParameters['resource_link_id']) || (strlen(trim($this->messageParameters['resource_link_id'])) <= 0)) {
- $this->setError('Missing resource link ID.', true, $generateWarnings);
+ $this->setError('Missing resource link ID', true, $generateWarnings);
}
if (!isset($this->messageParameters['custom_ap_attempt_number']) || (strlen(trim($this->messageParameters['custom_ap_attempt_number'])) <= 0) ||
!is_numeric($this->messageParameters['custom_ap_attempt_number'])) {
- $this->setError('Missing or invalid value for attempt number.', true, $generateWarnings);
+ $this->setError('Missing or invalid value for attempt number', true, $generateWarnings);
}
if (!isset($this->messageParameters['user_id']) || (strlen(trim($this->messageParameters['user_id'])) <= 0)) {
- $this->setError('Empty user ID.', true, $generateWarnings);
+ $this->setError('Empty user ID', true, $generateWarnings);
}
}
}
@@ -1444,7 +1456,7 @@ private function authenticate(bool $strictMode, bool $disableCookieCheck, bool $
if (isset($this->messageParameters['role_scope_mentor'])) {
if (!isset($this->messageParameters['roles']) ||
!in_array('urn:lti:role:ims/lis/Mentor', self::parseRoles($this->messageParameters['roles']))) {
- $this->setError('Found role_scope_mentor parameter without a Mentor role.', $strictMode, $generateWarnings);
+ $this->setError('Found role_scope_mentor parameter without a Mentor role', Util::$strictMode, $generateWarnings);
}
}
}
@@ -1453,13 +1465,13 @@ private function authenticate(bool $strictMode, bool $disableCookieCheck, bool $
($this->messageParameters['lti_message_type'] !== 'ToolProxyRegistrationRequest')) {
$now = time();
if (!isset($this->messageParameters['oauth_consumer_key'])) {
- $this->setError('Missing consumer key.', true, $generateWarnings);
+ $this->setError('Missing consumer key', true, $generateWarnings);
}
if (is_null($this->platform->created)) {
if (empty($this->jwt) || !$this->jwt->hasJwt()) {
- $reason = "Consumer key not recognised: '{$this->messageParameters['oauth_consumer_key']}'.";
+ $reason = "Consumer key not recognised: '{$this->messageParameters['oauth_consumer_key']}'";
} else {
- $reason = "Platform not recognised (Platform ID | Client ID | Deployment ID): '{$this->messageParameters['platform_id']}' | '{$this->messageParameters['oauth_consumer_key']}' | '{$this->messageParameters['deployment_id']}'.";
+ $reason = "Platform not recognised (Platform ID | Client ID | Deployment ID): '{$this->messageParameters['platform_id']}' | '{$this->messageParameters['oauth_consumer_key']}' | '{$this->messageParameters['deployment_id']}'";
}
$this->setError($reason, true, $generateWarnings);
}
@@ -1476,26 +1488,26 @@ private function authenticate(bool $strictMode, bool $disableCookieCheck, bool $
$doSavePlatform = $doSavePlatform || ($last !== $today);
}
$this->platform->lastAccess = $now;
- $this->ok = $this->verifySignature();
+ $this->verifySignature();
}
if ($this->ok) {
if ($this->platform->protected) {
if (!is_null($this->platform->consumerGuid)) {
$this->ok = empty($this->messageParameters['tool_consumer_instance_guid']) || ($this->platform->consumerGuid === $this->messageParameters['tool_consumer_instance_guid']);
if (!$this->ok) {
- $this->reason = 'Request is from an invalid platform.';
+ $this->setReason('Request is from an invalid platform');
}
} else {
$this->ok = isset($this->messageParameters['tool_consumer_instance_guid']);
if (!$this->ok) {
- $this->reason = 'A platform GUID must be included in the launch request as this configuration is protected.';
+ $this->setReason('A platform GUID must be included in the launch request as this configuration is protected');
}
}
}
if ($this->ok) {
$this->ok = $this->platform->enabled;
if (!$this->ok) {
- $this->reason = 'Platform has not been enabled by the tool.';
+ $this->setReason('Platform has not been enabled by the tool');
}
}
if ($this->ok) {
@@ -1503,10 +1515,10 @@ private function authenticate(bool $strictMode, bool $disableCookieCheck, bool $
if ($this->ok) {
$this->ok = is_null($this->platform->enableUntil) || ($this->platform->enableUntil > $now);
if (!$this->ok) {
- $this->reason = 'Platform access has expired.';
+ $this->setReason('Platform access has expired');
}
} else {
- $this->reason = 'Platform access is not yet available.';
+ $this->setReason('Platform access is not yet available');
}
}
}
@@ -1528,8 +1540,8 @@ private function authenticate(bool $strictMode, bool $disableCookieCheck, bool $
}
}
if (!$found) {
- $this->setError(sprintf('No valid value found in context_type parameter: \'%s\'.',
- $this->messageParameters['context_type']), $strictMode, $generateWarnings);
+ $this->setError(sprintf('No valid value found in context_type parameter: \'%s\'',
+ $this->messageParameters['context_type']), Util::$strictMode, $generateWarnings);
}
}
if (($this->ok || $generateWarnings) && !empty($this->messageParameters['lti_message_type']) &&
@@ -1537,55 +1549,54 @@ private function authenticate(bool $strictMode, bool $disableCookieCheck, bool $
$isUpdate = ($this->messageParameters['lti_message_type'] === 'ContentItemUpdateRequest');
if (isset($this->messageParameters['accept_unsigned']) &&
!$this->checkValue($this->messageParameters['accept_unsigned'], ['true', 'false'],
- 'Invalid value for accept_unsigned parameter: \'%s\'.', $strictMode, $generateWarnings)) {
+ 'Invalid value for accept_unsigned parameter: \'%s\'', $generateWarnings)) {
$this->ok = false;
}
if (isset($this->messageParameters['accept_multiple'])) {
if (!$isUpdate) {
if (!$this->checkValue($this->messageParameters['accept_multiple'], ['true', 'false'],
- 'Invalid value for accept_multiple parameter: \'%s\'.', $strictMode, $generateWarnings)) {
+ 'Invalid value for accept_multiple parameter: \'%s\'', $generateWarnings)) {
$this->ok = false;
}
} elseif (!$this->checkValue($this->messageParameters['accept_multiple'], ['false'],
- 'Invalid value for accept_multiple parameter: \'%s\'.', $strictMode, $generateWarnings)) {
+ 'Invalid value for accept_multiple parameter: \'%s\'', $generateWarnings)) {
$this->ok = false;
}
}
if (isset($this->messageParameters['accept_copy_advice'])) {
if (!$isUpdate) {
if (!$this->checkValue($this->messageParameters['accept_copy_advice'], ['true', 'false'],
- 'Invalid value for accept_copy_advice parameter: \'%s\'.', $strictMode, $generateWarnings)) {
+ 'Invalid value for accept_copy_advice parameter: \'%s\'', $generateWarnings)) {
$this->ok = false;
}
} elseif (!$this->checkValue($this->messageParameters['accept_copy_advice'], ['false'],
- 'Invalid value for accept_copy_advice parameter: \'%s\'.', $strictMode, $generateWarnings)) {
+ 'Invalid value for accept_copy_advice parameter: \'%s\'', $generateWarnings)) {
$this->ok = false;
}
}
if (isset($this->messageParameters['auto_create']) &&
!$this->checkValue($this->messageParameters['auto_create'], ['true', 'false'],
- 'Invalid value for auto_create parameter: \'%s\'.', $strictMode, $generateWarnings)) {
+ 'Invalid value for auto_create parameter: \'%s\'', $generateWarnings)) {
$this->ok = false;
}
if (isset($this->messageParameters['can_confirm']) &&
!$this->checkValue($this->messageParameters['can_confirm'], ['true', 'false'],
- 'Invalid value for can_confirm parameter: \'%s\'.', $strictMode, $generateWarnings)) {
+ 'Invalid value for can_confirm parameter: \'%s\'', $generateWarnings)) {
$this->ok = false;
}
}
if (isset($this->messageParameters['launch_presentation_document_target'])) {
if (!$this->checkValue($this->messageParameters['launch_presentation_document_target'],
['embed', 'frame', 'iframe', 'window', 'popup', 'overlay'],
- 'Invalid value for launch_presentation_document_target parameter: \'%s\'.', $strictMode,
- $generateWarnings, true)) {
+ 'Invalid value for launch_presentation_document_target parameter: \'%s\'', $generateWarnings, true)) {
$this->ok = false;
}
if (($this->messageParameters['lti_message_type'] === 'LtiStartProctoring') &&
($this->messageParameters['launch_presentation_document_target'] !== 'window')) {
if (isset($this->messageParameters['launch_presentation_height']) ||
isset($this->messageParameters['launch_presentation_width'])) {
- $this->setError('Height and width parameters must only be included for the window document target.',
- $strictMode, $generateWarnings);
+ $this->setError('Height and width parameters must only be included for the window document target',
+ Util::$strictMode, $generateWarnings);
}
}
}
@@ -1597,7 +1608,7 @@ private function authenticate(bool $strictMode, bool $disableCookieCheck, bool $
}
if (!empty($errors)) {
$this->setError(sprintf('Custom parameters must have string values: \'%s\'', implode('\', \'', $errors)),
- $strictMode, $generateWarnings);
+ Util::$strictMode, $generateWarnings);
}
}
}
@@ -1605,7 +1616,7 @@ private function authenticate(bool $strictMode, bool $disableCookieCheck, bool $
if ($this->ok && ($this->messageParameters['lti_message_type'] === 'ToolProxyRegistrationRequest')) {
$this->ok = $this->ltiVersion === LtiVersion::V2;
if (!$this->ok) {
- $this->reason = 'Invalid lti_version parameter.';
+ $this->setReason('Invalid \'lti_version\' parameter');
}
if ($this->ok) {
$url = $this->messageParameters['tc_profile_url'];
@@ -1618,12 +1629,12 @@ private function authenticate(bool $strictMode, bool $disableCookieCheck, bool $
$http = new HttpMessage($url, 'GET', null, 'Accept: application/vnd.ims.lti.v2.toolconsumerprofile+json');
$this->ok = $http->send();
if (!$this->ok) {
- $this->reason = 'Platform profile not accessible.';
+ $this->setReason('Platform profile not accessible');
} else {
$tcProfile = Util::jsonDecode($http->response);
$this->ok = !is_null($tcProfile);
if (!$this->ok) {
- $this->reason = 'Invalid JSON in platform profile.';
+ $this->setReason('Invalid JSON in platform profile');
}
}
}
@@ -1650,8 +1661,7 @@ private function authenticate(bool $strictMode, bool $disableCookieCheck, bool $
}
if (!empty($missing)) {
ksort($missing);
- $this->reason = 'Required capability not offered - \'' . implode('\', \'', array_keys($missing)) . '\'.';
- $this->ok = false;
+ $this->setReason('Required capability not offered - \'' . implode('\', \'', array_keys($missing)) . '\'');
}
}
// Check for required services
@@ -1660,15 +1670,18 @@ private function authenticate(bool $strictMode, bool $disableCookieCheck, bool $
foreach ($service->formats as $format) {
if (!$this->findService($format, $service->actions)) {
if ($this->ok) {
- $this->reason = 'Required service(s) not offered - ';
+ $reason = 'Required service(s) not offered - ';
$this->ok = false;
} else {
- $this->reason .= ', ';
+ $reason .= ', ';
}
- $this->reason .= "'{$format}' [" . implode(', ', $service->actions) . '].';
+ $reason .= "'{$format}' [" . implode(', ', $service->actions) . ']';
}
}
}
+ if (!$this->ok) {
+ $this->setReason($reason);
+ }
}
if ($this->ok) {
if ($this->messageParameters['lti_message_type'] === 'ToolProxyRegistrationRequest') {
@@ -1707,8 +1720,7 @@ private function authenticate(bool $strictMode, bool $disableCookieCheck, bool $
if (isset($this->messageParameters['relaunch_url'])) {
Util::logRequest();
if (empty($this->messageParameters['platform_state'])) {
- $this->ok = false;
- $this->reason = 'Missing or empty platform_state parameter.';
+ $this->setReason('Missing or empty \'platform_state\' parameter');
} else {
$this->ok = $this->sendRelaunchRequest($disableCookieCheck);
}
@@ -1733,10 +1745,7 @@ private function authenticate(bool $strictMode, bool $disableCookieCheck, bool $
}
}
if (count($invalidParameters) > 0) {
- $this->ok = false;
- if (empty($this->reason)) {
- $this->reason = 'Invalid parameter(s): ' . implode(', ', $invalidParameters) . '.';
- }
+ $this->setReason('Invalid parameter(s): ' . implode(', ', $invalidParameters));
}
if ($this->ok) {
@@ -1986,23 +1995,20 @@ private function authenticate(bool $strictMode, bool $disableCookieCheck, bool $
}
// Check if a share arrangement is in place for this resource link
- $this->ok = $this->checkForShare();
+ $this->checkForShare();
}
}
}
}
-
- return $this->ok;
}
/**
* Check if a share arrangement is in place.
*
- * @return bool True if no error is reported
+ * @return void
*/
- private function checkForShare(): bool
+ private function checkForShare(): void
{
- $ok = true;
$doSaveResourceLink = true;
$id = $this->resourceLink->primaryResourceLinkId;
@@ -2010,20 +2016,19 @@ private function checkForShare(): bool
$shareRequest = isset($this->messageParameters['custom_share_key']) && !empty($this->messageParameters['custom_share_key']);
if ($shareRequest) {
if (!$this->allowSharing) {
- $ok = false;
- $this->reason = 'Your sharing request has been refused because sharing is not being permitted.';
+ $this->setReason('Your sharing request has been refused because sharing is not being permitted');
} else {
// Check if this is a new share key
$shareKey = new ResourceLinkShareKey($this->resourceLink, $this->messageParameters['custom_share_key']);
if (!is_null($shareKey->resourceLinkId)) {
// Update resource link with sharing primary resource link details
$id = $shareKey->resourceLinkId;
- $ok = ($id !== $this->resourceLink->getRecordId());
- if ($ok) {
+ $this->ok = ($id !== $this->resourceLink->getRecordId());
+ if ($this->ok) {
$this->resourceLink->primaryResourceLinkId = $id;
$this->resourceLink->shareApproved = $shareKey->autoApprove;
- $ok = $this->resourceLink->save();
- if ($ok) {
+ $this->ok = $this->resourceLink->save();
+ if ($this->ok) {
$doSaveResourceLink = false;
$this->userResult->getResourceLink()->primaryResourceLinkId = $id;
$this->userResult->getResourceLink()->shareApproved = $shareKey->autoApprove;
@@ -2031,47 +2036,45 @@ private function checkForShare(): bool
// Remove share key
$shareKey->delete();
} else {
- $this->reason = 'An error occurred initialising your share arrangement.';
+ $this->setReason('An error occurred initialising your share arrangement');
}
} else {
- $this->reason = 'It is not possible to share your resource link with yourself.';
+ $this->setReason('It is not possible to share your resource link with yourself');
}
}
- if ($ok) {
- $ok = !is_null($id);
- if (!$ok) {
- $this->reason = 'You have requested to share a resource link but none is available.';
+ if ($this->ok) {
+ $this->ok = !is_null($id);
+ if (!$this->ok) {
+ $this->setReason('You have requested to share a resource link but none is available');
} else {
- $ok = (!is_null($this->userResult->getResourceLink()->shareApproved) && $this->userResult->getResourceLink()->shareApproved);
- if (!$ok) {
- $this->reason = 'Your share request is waiting to be approved.';
+ $this->ok = (!is_null($this->userResult->getResourceLink()->shareApproved) && $this->userResult->getResourceLink()->shareApproved);
+ if (!$this->ok) {
+ $this->setReason('Your share request is waiting to be approved');
}
}
}
}
} else {
// Check no share is in place
- $ok = is_null($id);
- if (!$ok) {
- $this->reason = 'You have not requested to share a resource link but an arrangement is currently in place.';
+ $this->ok = is_null($id);
+ if (!$this->ok) {
+ $this->setReason('You have not requested to share a resource link but an arrangement is currently in place');
}
}
// Look up primary resource link
- if ($ok && !is_null($id)) {
+ if ($this->ok && !is_null($id)) {
$resourceLink = ResourceLink::fromRecordId($id, $this->dataConnector);
- $ok = !is_null($resourceLink->created);
- if ($ok) {
+ $this->ok = !is_null($resourceLink->created);
+ if ($this->ok) {
if ($doSaveResourceLink) {
$this->resourceLink->save();
}
$this->resourceLink = $resourceLink;
} else {
- $this->reason = 'Unable to load resource link being shared.';
+ $this->setReason('Unable to load resource link being shared');
}
}
-
- return $ok;
}
/**
@@ -2101,7 +2104,7 @@ private function sendAuthenticationRequest(array $parameters, bool $disableCooki
}
$ok = !is_null($this->platform) && !empty($this->platform->authenticationUrl);
if (!$ok) {
- $this->reason = 'Platform not found or no platform authentication request URL.';
+ $this->setReason('Platform not found or no platform authentication request URL');
} else {
$oauthRequest = OAuth\OAuthRequest::from_request();
$usePlatformStorage = !empty($oauthRequest->get_parameter('lti_storage_target'));
@@ -2182,7 +2185,7 @@ private function sendAuthenticationRequest(array $parameters, bool $disableCooki
Util::redirect($this->platform->authenticationUrl, $params, '', $javascript);
}
} else {
- $this->reason = 'Unable to generate a state value.';
+ $this->setReason('Unable to generate a state value');
}
}
@@ -2226,7 +2229,7 @@ private function sendRelaunchRequest(bool $disableCookieCheck): bool
$params = $this->platform->addSignature($this->messageParameters['relaunch_url'], $params);
$this->output = Util::sendForm($this->messageParameters['relaunch_url'], $params);
} else {
- $this->reason = 'Unable to generate a state value.';
+ $this->setReason('Unable to generate a state value');
}
return $this->ok;
@@ -2238,34 +2241,37 @@ private function sendRelaunchRequest(bool $disableCookieCheck): bool
* @param string $value Value to be checked
* @param array $values Array of permitted values
* @param string $reason Reason to generate when the value is not permitted
- * @param bool $strictMode True if full compliance with the LTI specification is required
* @param bool $generateWarnings True if warning messages should be generated
* @param bool $ignoreInvalid True if invalid values are to be ignored (optional default is false)
*
* @return bool True if value is valid
*/
- private function checkValue(string &$value, array $values, string $reason, bool $strictMode, bool $generateWarnings,
- bool $ignoreInvalid = false): bool
+ private function checkValue(?string &$value, array $values, string $reason, bool $generateWarnings, bool $ignoreInvalid = false): bool
{
- $lookupValue = $value;
- if (!$strictMode) {
- $lookupValue = strtolower($value);
- }
- $ok = in_array($lookupValue, $values);
- if (!$ok) {
- if ($this->ok && $strictMode) {
- $this->reason = sprintf($reason, $value);
- } else {
- $ok = true;
+ $ok = true;
+ if (!empty($value)) {
+ $lookupValue = $value;
+ if (!Util::$strictMode) {
+ $lookupValue = strtolower($value);
+ }
+ $ok = in_array($lookupValue, $values);
+ if (!$ok) {
+ if ($this->ok && Util::$strictMode) {
+ $this->setReason(sprintf($reason, $value));
+ } else {
+ $ok = true;
+ if ($generateWarnings) {
+ $this->warnings[] = sprintf($reason, $value);
+ Util::setMessage(false, sprintf($reason, $value));
+ }
+ }
+ } elseif ($lookupValue !== $value) {
if ($generateWarnings) {
- $this->warnings[] = sprintf($reason, $value);
+ $this->warnings[] = sprintf($reason, $value) . " [Changed to '{$lookupValue}']";
+ Util::setMessage(false, sprintf($reason, $value) . " [Changed to '{$lookupValue}']");
}
+ $value = $lookupValue;
}
- } elseif ($lookupValue !== $value) {
- if ($generateWarnings) {
- $this->warnings[] = sprintf($reason, $value) . " [Changed to '{$lookupValue}']";
- }
- $value = $lookupValue;
}
return $ok;
@@ -2282,11 +2288,11 @@ private function checkValue(string &$value, array $values, string $reason, bool
*/
private function setError(string $reason, bool $strictMode, bool $generateWarnings): void
{
- if ($strictMode && $this->ok) {
- $this->ok = false;
- $this->reason = $reason;
+ if ($strictMode) {
+ $this->setReason($reason);
} elseif ($generateWarnings) {
$this->warnings[] = $reason;
+ Util::setMessage(false, $reason);
}
}
diff --git a/src/Util.php b/src/Util.php
index a8a3e9d..af3cc44 100644
--- a/src/Util.php
+++ b/src/Util.php
@@ -5,6 +5,7 @@
use ceLTIc\LTI\OAuth;
use ceLTIc\LTI\Enum\LogLevel;
+use ceLTIc\LTI\Jwt\ClientInterface;
/**
* Class to implement utility methods
@@ -44,7 +45,7 @@ final class Util
'auto_create' => ['suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'auto_create', 'isBoolean' => true],
'can_confirm' => ['suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'can_confirm'],
'content_item_return_url' => ['suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'deep_link_return_url'],
- 'content_items' => ['suffix' => 'dl', 'group' => '', 'claim' => 'content_items', 'isObject' => true],
+ 'content_items' => ['suffix' => 'dl', 'group' => '', 'claim' => 'content_items', 'isContentItemSelection' => true],
'data' => ['suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'data'],
'data.LtiDeepLinkingResponse' => ['suffix' => 'dl', 'group' => '', 'claim' => 'data'],
'text' => ['suffix' => 'dl', 'group' => 'deep_linking_settings', 'claim' => 'text'],
@@ -77,6 +78,8 @@ final class Util
'role_scope_mentor' => ['suffix' => '', 'group' => '', 'claim' => 'role_scope_mentor', 'isArray' => true],
'platform_id' => ['suffix' => '', 'group' => null, 'claim' => 'iss'],
'deployment_id' => ['suffix' => '', 'group' => '', 'claim' => 'deployment_id'],
+ 'oauth_nonce' => ['suffix' => '', 'group' => null, 'claim' => 'nonce'],
+ 'oauth_timestamp' => ['suffix' => '', 'group' => null, 'claim' => 'iat', 'isInteger' => true],
'lti_message_type' => ['suffix' => '', 'group' => '', 'claim' => 'message_type'],
'lti_version' => ['suffix' => '', 'group' => '', 'claim' => 'version'],
'resource_link_description' => ['suffix' => '', 'group' => 'resource_link', 'claim' => 'description'],
@@ -163,6 +166,13 @@ final class Util
*/
public static LogLevel $logLevel = LogLevel::None;
+ /**
+ * Whether full compliance with the LTI specification is required.
+ *
+ * @var bool $strictMode
+ */
+ public static bool $strictMode = false;
+
/**
* Delay (in seconds) before a manual button is displayed in case a browser is blocking a form submission.
*
@@ -170,6 +180,13 @@ final class Util
*/
public static int $formSubmissionTimeout = 2;
+ /**
+ * Messages relating to service request.
+ *
+ * @var array $messages
+ */
+ private static array $messages = [true => [], false => []];
+
/**
* Check whether the request received could be an LTI message.
*
@@ -329,6 +346,33 @@ public static function log(string $message, bool $showSource = false): void
error_log($message . $source);
}
+ /**
+ * Set an error or warning message.
+ *
+ * @param bool $isError True if the message represents an error
+ * @param string $message Message
+ *
+ * @return void
+ */
+ public static function setMessage(bool $isError, string $message): void
+ {
+ if (!in_array($message, self::$messages[$isError])) {
+ self::$messages[$isError][] = $message;
+ }
+ }
+
+ /**
+ * Get the system error or warning messages.
+ *
+ * @param bool $isError True if error messages are to be returned
+ *
+ * @return array Array of messages
+ */
+ public static function getMessages(bool $isError): array
+ {
+ return self::$messages[$isError];
+ }
+
/**
* Generate a web page containing an auto-submitted form of parameters.
*
@@ -540,17 +584,20 @@ public static function urlEncode(?string $val): string
/**
* Convert a value to a string.
*
- * @param mixed $val The value to be converted
+ * @param mixed $val The value to be converted
+ * @param string|null $default Value to return when a conversion is not possible (optional, default is an empty string)
*
* @return string
*/
- public static function valToString(mixed $val): string
+ public static function valToString(mixed $val, ?string $default = ''): string
{
if (!is_string($val)) {
- if (is_scalar($val)) {
+ if (is_bool($val)) {
+ $val = ($val) ? 'true' : 'false';
+ } elseif (is_scalar($val)) {
$val = strval($val);
} else {
- $val = '';
+ $val = $default;
}
}
@@ -589,16 +636,309 @@ public static function valToNumber(mixed $val): int|float|null
public static function valToBoolean(mixed $val): bool
{
if (!is_bool($val)) {
- if ($val !== 'true') {
- $val = false;
- } else {
+ if (($val === 'true' || $val === 1)) {
$val = true;
+ } else {
+ $val = false;
}
}
return $val;
}
+ /**
+ * Get the named object element from object.
+ *
+ * @param object $obj Object containing the element
+ * @param string $fullname Name of element
+ * @param bool $required True if the element must be present
+ * @param bool $stringValues True if the values must be strings
+ *
+ * @return object|null Value of element (or null if not found)
+ */
+ public static function checkObject(object $obj, string $fullname, bool $required = false, bool $stringValues = false): ?object
+ {
+ $element = null;
+ $name = $fullname;
+ $pos = strrpos($name, '/');
+ if ($pos !== false) {
+ $name = substr($name, $pos + 1);
+ }
+ if (isset($obj->{$name})) {
+ if (is_object($obj->{$name})) {
+ $element = $obj->{$name};
+ if ($stringValues) {
+ foreach (get_object_vars($element) as $elementName => $elementValue) {
+ if (!is_string($elementValue)) {
+ if (!self::$strictMode) {
+ self::setMessage(false,
+ "Properties of the {$fullname} element should have a string value (" . gettype($elementValue) . ' found)');
+ $element->{$elementName} = self::valToString($elementValue);
+ } else {
+ $element = null;
+ self::setMessage(true,
+ "Properties of the {$fullname} element must have a string value (" . gettype($elementValue) . ' found)');
+ break;
+ }
+ }
+ }
+ }
+ } else {
+ self::setMessage(false, "The '{$fullname}' element must be an object (" . gettype($obj->{$name}) . ' found)');
+ }
+ } elseif ($required) {
+ self::setMessage(true, "The '{$fullname}' element is missing");
+ }
+
+ return $element;
+ }
+
+ /**
+ * Get the named array element from object.
+ *
+ * @param object $obj Object containing the element
+ * @param string $fullname Name of element
+ * @param bool $required True if the element must be present
+ * @param bool $notEmpty True if the element must not have an empty value
+ *
+ * @return array Value of element (or empty string if not found)
+ */
+ public static function checkArray(object $obj, string $fullname, bool $required = false, bool $notEmpty = false): array
+ {
+ $arr = [];
+ $name = $fullname;
+ $pos = strrpos($name, '/');
+ if ($pos !== false) {
+ $name = substr($name, $pos + 1);
+ }
+ if (isset($obj->{$name})) {
+ if (is_array($obj->{$name})) {
+ $arr = $obj->{$name};
+ if ($notEmpty && empty($arr)) {
+ self::setMessage(true, "The '{$fullname}' element must not be empty");
+ }
+ } elseif (self::$strictMode) {
+ self::setMessage(false, "The '{$fullname}' element must have an array value (" . gettype($obj->{$name}) . ' found)');
+ } else {
+ self::setMessage(false,
+ "The '{$fullname}' element should have an array value (" . gettype($obj->{$name}) . ' found)');
+ if (is_object($obj->{$name})) {
+ $arr = (array) $obj->{$name};
+ }
+ }
+ } elseif ($required) {
+ self::setMessage(true, "The '{$fullname}' element is missing");
+ }
+
+ return $arr;
+ }
+
+ /**
+ * Get the named string element from object.
+ *
+ * @param object $obj Object containing the element
+ * @param string $fullname Name of element (may include a path)
+ * @param bool $required True if the element must be present
+ * @param bool $notEmpty True if the element must not have an empty value
+ * @param string $fixedValue Required value of element (empty string if none)
+ * @param bool $overrideStrictMode Ignore strict mode setting
+ * @param string|null $default Value to return when a conversion is not possible (optional, default is an empty string)
+ *
+ * @return string Value of element (or default value if not found or valid)
+ */
+ public static function checkString(object $obj, string $fullname, bool $required = false, bool $notEmpty = false,
+ string|array $fixedValue = '', bool $overrideStrictMode = false, ?string $default = ''): ?string
+ {
+ $value = $default;
+ $name = $fullname;
+ $pos = strrpos($name, '/');
+ if ($pos !== false) {
+ $name = substr($name, $pos + 1);
+ }
+ if (isset($obj->{$name})) {
+ if (is_string($obj->{$name})) {
+ if (!empty($fixedValue) && is_string($fixedValue) && ($obj->{$name} !== $fixedValue)) {
+ if (self::$strictMode || $overrideStrictMode) {
+ self::setMessage(true,
+ "The '{$fullname}' element must have a value of '{$fixedValue}' ('{$obj->{$name}}' found)");
+ } else {
+ self::setMessage(false,
+ "The '{$fullname}' element should have a value of '{$fixedValue}' ('{$obj->{$name}}' found)");
+ }
+ } elseif (!empty($fixedValue) && is_array($fixedValue) && !in_array($obj->{$name}, $fixedValue)) {
+ self::setMessage(self::$strictMode || $overrideStrictMode,
+ "Value of the '{$fullname}' element not recognised ('{$obj->{$name}}' found)");
+ } elseif ($notEmpty && empty($obj->{$name})) {
+ if (self::$strictMode || $overrideStrictMode) {
+ self::setMessage(true, "The '{$fullname}' element must not be empty");
+ } else {
+ self::setMessage(false, "The '{$fullname}' element should not be empty");
+ }
+ } else {
+ $value = $obj->{$name};
+ }
+ } elseif ($required || !is_null($obj->{$name})) {
+ if (self::$strictMode || $overrideStrictMode) {
+ self::setMessage(true,
+ "The '{$fullname}' element must have a string value (" . gettype($obj->{$name}) . ' found)');
+ } else {
+ self::setMessage(false,
+ "The '{$fullname}' element should have a string value (" . gettype($obj->{$name}) . ' found)');
+ $value = self::valToString($obj->{$name}, $default);
+ }
+ }
+ } elseif ($required) {
+ self::setMessage(true, "The '{$fullname}' element is missing");
+ }
+
+ return $value;
+ }
+
+ /**
+ * Get the named number element from object.
+ *
+ * @param object $obj Object containing the element
+ * @param string $fullname Name of element (may include a path)
+ * @param bool $required True if the element must be present
+ * @param int|false $minimum Minimum value (or false is none)
+ * @param bool $minimumExclusive True if value must exceed the minimum
+ * @param bool $overrideStrictMode Ignore strict mode setting
+ *
+ * @return int|float|null Value of element (or null if not found or valid)
+ */
+ public static function checkNumber(object $obj, string $fullname, bool $required = false, int|false $minimum = false,
+ bool $minimumExclusive = false, bool $overrideStrictMode = false): int|float|null
+ {
+ $value = null;
+ $name = $fullname;
+ $pos = strrpos($name, '/');
+ if ($pos !== false) {
+ $name = substr($name, $pos + 1);
+ }
+ if (isset($obj->{$name})) {
+ if (is_int($obj->{$name}) || is_float($obj->{$name})) {
+ if (($minimum !== false) && !$minimumExclusive && ($obj->{$name} < $minimum)) {
+ self::setMessage(true, "The '{$fullname}' element must have a numeric value of at least {$minimum}");
+ } elseif (($minimum !== false) && $minimumExclusive && ($obj->{$name} <= $minimum)) {
+ self::setMessage(true, "The '{$fullname}' element must have a numeric value greater than {$minimum}");
+ } else {
+ $value = $obj->{$name};
+ }
+ } elseif ($required || !is_null($obj->{$name})) {
+ if (self::$strictMode || $overrideStrictMode) {
+ self::setMessage(true,
+ "The '{$fullname}' element must have a numeric value (" . gettype($obj->{$name}) . ' found)');
+ } else {
+ self::setMessage(false,
+ "The '{$fullname}' element should have a numeric value (" . gettype($obj->{$name}) . ' found)');
+ $value = self::valToNumber($obj->{$name});
+ }
+ }
+ } elseif ($required) {
+ self::setMessage(true, "The '{$fullname}' element is missing");
+ }
+
+ return $value;
+ }
+
+ /**
+ * Get the named integer element from object.
+ *
+ * @param object $obj Object containing the element
+ * @param string $fullname Name of element (may include a path)
+ * @param bool $required True if the element must be present
+ * @param int|false $minimum Minimum value (or false is none)
+ * @param bool $minimumExclusive True if value must exceed the minimum
+ * @param bool $overrideStrictMode Ignore strict mode setting
+ *
+ * @return int|null Value of element (or null if not found or valid)
+ */
+ public static function checkInteger(object $obj, string $fullname, bool $required = false, int|false $minimum = false,
+ bool $minimumExclusive = false, bool $overrideStrictMode = false): int|null
+ {
+ $value = self::checkNumber($obj, $fullname, $required, $minimum, $minimumExclusive, $overrideStrictMode);
+ if (is_float($value)) {
+ if (self::$strictMode) {
+ self::setMessage(true, "The '{$fullname}' element must have an integer value");
+ $value = null;
+ } else {
+ self::setMessage(false, "The '{$fullname}' element should have an integer value");
+ $value = intval($value);
+ }
+ }
+
+ return $value;
+ }
+
+ /**
+ * Get the named boolean element from object.
+ *
+ * @param object $obj Object containing the element
+ * @param string $fullname Name of element (may include a path)
+ * @param bool $required True if the element must be present
+ * @param string|null $default Value to return when a conversion is not possible (optional, default is an empty string)
+ *
+ * @return bool|null Value of element (or null if not found or valid)
+ */
+ public static function checkBoolean(object $obj, string $fullname, bool $required = false, ?bool $default = null): ?bool
+ {
+ $value = $default;
+ $name = $fullname;
+ $pos = strrpos($name, '/');
+ if ($pos !== false) {
+ $name = substr($name, $pos + 1);
+ }
+ if (isset($obj->{$name})) {
+ if (is_bool($obj->{$name})) {
+ $value = $obj->{$name};
+ } elseif (!self::$strictMode) {
+ self::setMessage(false,
+ "The '{$fullname}' element should have a boolean value (" . gettype($obj->{$name}) . ' found)');
+ $value = self::valToBoolean($obj->{$name});
+ } else {
+ self::setMessage(true, "The '{$fullname}' element must have a boolean value (" . gettype($obj->{$name}) . ' found)');
+ }
+ } elseif ($required) {
+ self::setMessage(true, "The '{$fullname}' element is missing");
+ }
+
+ return $value;
+ }
+
+ /**
+ * Get the named number element from object.
+ *
+ * @param object $obj Object containing the element
+ * @param string $fullname Name of element (may include a path)
+ * @param bool $required True if the element must be present
+ *
+ * @return int Value of element (or 0 if not found or valid)
+ */
+ public static function checkDateTime(object $obj, string $fullname, bool $required = false): int
+ {
+ $value = 0;
+ $name = $fullname;
+ $pos = strrpos($name, '/');
+ if ($pos !== false) {
+ $name = substr($name, $pos + 1);
+ }
+ if (isset($obj->{$name})) {
+ if (is_string($obj->{$name})) {
+ $value = strtotime($obj->{$name});
+ if ($value === false) {
+ self::setMessage(true, "The '{$fullname}' element must have a datetime value");
+ $value = 0;
+ }
+ } else {
+ self::setMessage(true, "The '{$fullname}' element must have a string value (" . gettype($obj->{$name}) . ' found)');
+ }
+ } elseif ($required) {
+ self::setMessage(true, "The '{$fullname}' element is missing");
+ }
+
+ return $value;
+ }
+
/**
* Decode a JSON string.
*