Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug fixes #10

Merged
merged 8 commits into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions NP.Lti13Platform.Core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,43 +96,43 @@ builder.Services

The platform information to identify the platform server, contacts, etc.

<hr>
***

`Guid`

A stable locally unique to the iss identifier for an instance of the tool platform. The value of guid is a case-sensitive string that MUST NOT exceed 255 ASCII characters in length. The use of Universally Unique IDentifier (UUID) defined in [RFC4122](https://www.rfc-editor.org/rfc/rfc4122) is recommended.

<hr>
***

`ContactEmail`

Administrative contact email for the platform instance.

<hr>
***

`Description`

Descriptive phrase for the platform instance.

<hr>
***

`Name`

Name for the platform instance.

<hr>
***

`Url`

Home HTTPS URL endpoint for the platform instance.

<hr>
***

`ProductFamilyCode`

Vendor product family code for the type of platform.

<hr>
***

`Version`

Expand All @@ -142,25 +142,25 @@ Vendor product version for the platform.

The configuration for handling of tokens between the platform and the tools.

<hr>
***

`Issuer`

A case-sensitive URL using the HTTPS scheme that contains: scheme, host; and, optionally, port number, and path components; and, no query or fragment components. The issuer identifies the platform to the tools.

<hr>
***

`TokenAudience`

The value used to validate a token request from a tool. This is used to compare against the 'aud' claim of that JWT token request. If not provided, the token endpoint url will be used as a fallback.

<hr>
***

`MessageTokenExpirationSeconds` Default: `300`

The expiration time of the lti messages that are sent to the tools.

<hr>
***

`AccessTokenExpirationSeconds` Default: `3600`

Expand Down
13 changes: 11 additions & 2 deletions NP.Lti13Platform.Core/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,15 @@ internal record AuthenticationRequest(string? Scope, string? Response_Type, stri

internal record TokenRequest(string Grant_Type, string Client_Assertion_Type, string Client_Assertion, string Scope);

/// <param name="DocumentTarget"> <see cref="Lti13PresentationTargetDocuments"/> has the list of possible values. </param>
public record LaunchPresentationOverride(string? DocumentTarget, double? Height, double? Width, string? ReturnUrl, string? Locale);
public record LaunchPresentationOverride
{
/// <summary>
/// <see cref="Lti13PresentationTargetDocuments"/> has the list of possible values.
/// </summary>
public string? DocumentTarget { get; set; }
public double? Height { get; set; }
public double? Width { get; set; }
public string? ReturnUrl { get; set; }
public string? Locale { get; set; }
}
}
14 changes: 13 additions & 1 deletion NP.Lti13Platform.DeepLinking/DeepLinkSettingsOverride.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
namespace NP.Lti13Platform.DeepLinking
{
public record DeepLinkSettingsOverride(string? DeepLinkReturnUrl, IEnumerable<string>? AcceptTypes, IEnumerable<string>? AcceptPresentationDocumentTargets, IEnumerable<string>? AcceptMediaTypes, bool? AcceptMultiple, bool? AcceptLineItem, bool? AutoCreate, string? Title, string? Text, string? Data);
public record DeepLinkSettingsOverride
{
public string? DeepLinkReturnUrl { get; set; }
public IEnumerable<string>? AcceptTypes { get; set; }
public IEnumerable<string>? AcceptPresentationDocumentTargets { get; set; }
public IEnumerable<string>? AcceptMediaTypes { get; set; }
public bool? AcceptMultiple { get; set; }
public bool? AcceptLineItem { get; set; }
public bool? AutoCreate { get; set; }
public string? Title { get; set; }
public string? Text { get; set; }
public string? Data { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public class DeepLinkSettingsMessage
}
}

public class DeepLinkingPopulator(LinkGenerator linkGenerator, IDepLinkingService deepLinkingService) : Populator<IDeepLinkingMessage>
public class DeepLinkingPopulator(LinkGenerator linkGenerator, IDeepLinkingService deepLinkingService) : Populator<IDeepLinkingMessage>
{
public override async Task PopulateAsync(IDeepLinkingMessage obj, MessageScope scope, CancellationToken cancellationToken = default)
{
Expand Down
22 changes: 11 additions & 11 deletions NP.Lti13Platform.DeepLinking/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,11 @@ app.UseLti13PlatformDeepLinking(config => {
});
```

### IDepLinkingService
### IDeepLinkingService

The `IDepLinkingService` interface is used to get the config for the deep linking service as well as handle the response from the tool. The config is used to control how deep link requests are made and how the response will be handled.
The `IDeepLinkingService` interface is used to get the config for the deep linking service as well as handle the response from the tool. The config is used to control how deep link requests are made and how the response will be handled.

There is a default implementation of the `IDepLinkingService` interface that uses a configuration set up on app start. When calling the `AddDefaultDeepLinkingService` method, the configuration can be setup at that time. A fallback to the current request scheme and host will be used if no ServiceAddress is configured. The Default implementation can be overridden by adding a new implementation of the `IDepLinkingService` interface and not including the Default.
There is a default implementation of the `IDeepLinkingService` interface that uses a configuration set up on app start. When calling the `AddDefaultDeepLinkingService` method, the configuration can be setup at that time. A fallback to the current request scheme and host will be used if no ServiceAddress is configured. The Default implementation can be overridden by adding a new implementation of the `IDeepLinkingService` interface and not including the Default.

```csharp
builder.Services
Expand All @@ -78,49 +78,49 @@ Then default handling of the response is to return it as an 200 OK response with

The configuration for the Deep Linking service tells the tools what kinds of things the platform is looking for and how it will handle the items when they are returned.

<hr>
***

`AcceptPresentationDocumentTargets` Default: `["embed", "iframe", "window"]`{:csharp}

Defines how the content items will be shown to users (Embedded, Iframe, Window).

<hr>
***

`AcceptTypes` Default: `["file", "html", "image", "link", "ltiResourceLink"]`{:csharp}

Defines which types of content items the platform is looking for (File, Html, Image, Link, ResourceLink).

<hr>
***

`AcceptMediaTypes` Default: `["image/*", "text/html"]`{:csharp}

Defines which media types the platform is looking for (image/*, text/html).

<hr>
***

`AcceptLineItem` Default: `true`{:csharp}

Whether the platform in the context of that deep linking request supports or ignores line items included in LTI Resource Link items. False indicates line items will be ignored. True indicates the platform will create a line item when creating the resource link. If the field is not present, no assumption that can be made about the support of line items.

<hr>
***

`AcceptMultiple` Default: `true`{:csharp}

Whether the platform allows multiple content items to be submitted in a single response.

<hr>
***

`AutoCreate` Default: `true`{:csharp}

Whether any content items returned by the tool would be automatically persisted without any option for the user to cancel the operation.

<hr>
***

`ServiceAddress` Default: `null`{:csharp}

The web address where the deep linking responses will be handled. If not set, the current request scheme and host will be used.

<hr>
***

`ContentItemTypes` Default: `[]`{:csharp}

Expand Down
4 changes: 2 additions & 2 deletions NP.Lti13Platform.DeepLinking/Services/DeepLinkingService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

namespace NP.Lti13Platform.DeepLinking.Services
{
internal class DeepLinkingService(IOptionsMonitor<DeepLinkingConfig> config, IHttpContextAccessor httpContextAccessor) : IDepLinkingService
internal class DeepLinkingService(IOptionsMonitor<DeepLinkingConfig> config, IHttpContextAccessor httpContextAccessor) : IDeepLinkingService
{
public Task<IResult> HandleResponseAsync(DeepLinkResponse response, CancellationToken cancellationToken = default) => Task.FromResult(Results.Ok(response));
public Task<IResult> HandleResponseAsync(string clientId, string deploymentId, string? contextId, DeepLinkResponse response, CancellationToken cancellationToken = default) => Task.FromResult(Results.Ok(response));

public async Task<DeepLinkingConfig> GetConfigAsync(string clientId, CancellationToken cancellationToken = default)
{
Expand Down
4 changes: 2 additions & 2 deletions NP.Lti13Platform.DeepLinking/Services/IDeepLinkingService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@

namespace NP.Lti13Platform.DeepLinking.Services
{
public interface IDepLinkingService
public interface IDeepLinkingService
{
Task<IResult> HandleResponseAsync(DeepLinkResponse response, CancellationToken cancellationToken = default);
Task<IResult> HandleResponseAsync(string clientId, string deploymentId, string? contextId, DeepLinkResponse response, CancellationToken cancellationToken = default);

Task<DeepLinkingConfig> GetConfigAsync(string clientId, CancellationToken cancellationToken = default);
}
Expand Down
46 changes: 25 additions & 21 deletions NP.Lti13Platform.DeepLinking/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public static Lti13PlatformBuilder AddDefaultDeepLinkingService(this Lti13Platfo
configure ??= x => { };

builder.Services.Configure(configure);
builder.Services.AddTransient<IDepLinkingService, DeepLinkingService>();
builder.Services.AddTransient<IDeepLinkingService, DeepLinkingService>();
return builder;
}

Expand All @@ -46,8 +46,8 @@ public static IEndpointRouteBuilder UseLti13PlatformDeepLinking(this IEndpointRo
var config = new DeepLinkingEndpointsConfig();
configure?.Invoke(config);

app.MapPost(config.DeepLinkingResponseUrl,
async ([FromForm] DeepLinkResponseRequest request, string? contextId, ILogger<DeepLinkResponseRequest> logger, ITokenService tokenService, ICoreDataService coreDataService, IDeepLinkingDataService deepLinkingDataService, IDepLinkingService deepLinkingService, CancellationToken cancellationToken) =>
_ = app.MapPost(config.DeepLinkingResponseUrl,
async ([FromForm] DeepLinkResponseRequest request, string? contextId, ILogger<DeepLinkResponseRequest> logger, ITokenService tokenService, ICoreDataService coreDataService, IDeepLinkingDataService deepLinkingDataService, IDeepLinkingService deepLinkingService, CancellationToken cancellationToken) =>
{
const string DEEP_LINKING_SPEC = "https://www.imsglobal.org/spec/lti-dl/v2p0/#deep-linking-response-message";
const string INVALID_CLIENT = "invalid_client";
Expand Down Expand Up @@ -115,20 +115,24 @@ public static IEndpointRouteBuilder UseLti13PlatformDeepLinking(this IEndpointRo

var deepLinkingConfig = await deepLinkingService.GetConfigAsync(tool.ClientId, cancellationToken);

List<(ContentItem ContentItem, LtiResourceLinkContentItem? LtiResourceLink)> contentItems = validatedToken.ClaimsIdentity.FindAll("https://purl.imsglobal.org/spec/lti-dl/claim/content_items")
.Select((x, ix) =>
{
var type = JsonDocument.Parse(x.Value).RootElement.GetProperty(TYPE).GetString() ?? UNKNOWN;
var customItem = (ContentItem)JsonSerializer.Deserialize(x.Value, deepLinkingConfig.ContentItemTypes[(tool.ClientId, type)])!;

return (customItem, type == ContentItemType.LtiResourceLink ? JsonSerializer.Deserialize<LtiResourceLinkContentItem>(x.Value) : null);
})
.ToList();

var response = new DeepLinkResponse
{
Data = validatedToken.ClaimsIdentity.FindFirst("https://purl.imsglobal.org/spec/lti-dl/claim/data")?.Value,
Message = validatedToken.ClaimsIdentity.FindFirst("https://purl.imsglobal.org/spec/lti-dl/claim/msg")?.Value,
Log = validatedToken.ClaimsIdentity.FindFirst("https://purl.imsglobal.org/spec/lti-dl/claim/log")?.Value,
ErrorMessage = validatedToken.ClaimsIdentity.FindFirst("https://purl.imsglobal.org/spec/lti-dl/claim/errormsg")?.Value,
ErrorLog = validatedToken.ClaimsIdentity.FindFirst("https://purl.imsglobal.org/spec/lti-dl/claim/errorlog")?.Value,
ContentItems = validatedToken.ClaimsIdentity.FindAll("https://purl.imsglobal.org/spec/lti-dl/claim/content_items")
.Select((x, ix) =>
{
var type = JsonDocument.Parse(x.Value).RootElement.GetProperty(TYPE).GetString() ?? UNKNOWN;
return (ContentItem)JsonSerializer.Deserialize(x.Value, deepLinkingConfig.ContentItemTypes[(tool.ClientId, type)])!;
})
.ToList()
ContentItems = contentItems.Select(ci => ci.ContentItem),
};

if (!string.IsNullOrWhiteSpace(response.Log))
Expand All @@ -143,33 +147,33 @@ public static IEndpointRouteBuilder UseLti13PlatformDeepLinking(this IEndpointRo

if (deepLinkingConfig.AutoCreate == true)
{
var saveTasks = response.ContentItems.Select(async ci =>
var saveTasks = contentItems.Select(async ci =>
{
var id = await deepLinkingDataService.SaveContentItemAsync(deployment.Id, contextId, ci);
var id = await deepLinkingDataService.SaveContentItemAsync(deployment.Id, contextId, ci.ContentItem);

if (ci is LtiResourceLinkContentItem rlci && deepLinkingConfig.AcceptLineItem == true && rlci.LineItem != null && contextId != null)
if (deepLinkingConfig.AcceptLineItem == true && contextId != null && ci.LtiResourceLink?.LineItem != null)
{
await deepLinkingDataService.SaveLineItemAsync(new LineItem
{
Id = string.Empty,
DeploymentId = deployment.Id,
ContextId = contextId,
Label = rlci.LineItem!.Label ?? rlci.Title ?? rlci.Type,
ScoreMaximum = rlci.LineItem.ScoreMaximum,
GradesReleased = rlci.LineItem.GradesReleased,
Tag = rlci.LineItem.Tag,
ResourceId = rlci.LineItem.ResourceId,
Label = ci.LtiResourceLink.LineItem!.Label ?? ci.LtiResourceLink.Title ?? ci.LtiResourceLink.Type,
ScoreMaximum = ci.LtiResourceLink.LineItem.ScoreMaximum,
GradesReleased = ci.LtiResourceLink.LineItem.GradesReleased,
Tag = ci.LtiResourceLink.LineItem.Tag,
ResourceId = ci.LtiResourceLink.LineItem.ResourceId,
ResourceLinkId = id,
StartDateTime = rlci.Submission?.StartDateTime?.UtcDateTime,
EndDateTime = rlci.Submission?.EndDateTime?.UtcDateTime
StartDateTime = ci.LtiResourceLink.Submission?.StartDateTime?.UtcDateTime,
EndDateTime = ci.LtiResourceLink.Submission?.EndDateTime?.UtcDateTime
});
}
});

await Task.WhenAll(saveTasks);
}

return await deepLinkingService.HandleResponseAsync(response, cancellationToken);
return await deepLinkingService.HandleResponseAsync(tool.ClientId, deployment.Id, contextId, response, cancellationToken);
})
.WithName(RouteNames.DEEP_LINKING_RESPONSE)
.DisableAntiforgery();
Expand Down
26 changes: 24 additions & 2 deletions NP.Lti13Platform.WebExample/Controllers/HomeController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,31 @@ public async Task<IResult> Index(CancellationToken cancellationToken)

return Results.Ok(new
{
deepLinkUrl = await service.GetDeepLinkInitiationUrlAsync(tool!, deployment!.Id, userId, false, null, context!.Id, new DeepLinkSettingsOverride(null, null, null, null, null, null, null, "TiTlE", "TEXT", "data"), cancellationToken: cancellationToken),
deepLinkUrl = await service.GetDeepLinkInitiationUrlAsync(
tool!,
deployment!.Id,
userId,
false,
null,
context!.Id,
new DeepLinkSettingsOverride { Title = "TiTlE", Text = "TEXT", Data = "data" },
cancellationToken: cancellationToken),
resourceLinkUrls = DataService.ResourceLinks
.Select(async resourceLink => await service.GetResourceLinkInitiationUrlAsync(tool!, deployment!.Id, context!.Id, resourceLink, userId, false, launchPresentation: new LaunchPresentationOverride(documentTarget, height, width, "", locale), cancellationToken: cancellationToken))
.Select(async resourceLink => await service.GetResourceLinkInitiationUrlAsync(
tool!,
deployment!.Id,
context!.Id,
resourceLink,
userId,
false,
launchPresentation: new LaunchPresentationOverride
{
DocumentTarget = documentTarget,
Height = height,
Width = width,
Locale = locale
},
cancellationToken: cancellationToken))
.Select(t => t.Result)
});
}
Expand Down
2 changes: 1 addition & 1 deletion NP.Lti13Platform.WebExample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
builder.Services.AddControllersWithViews();

builder.Services
.AddLti13PlatformWithDefaults(x => { x.Issuer = "https://mytest.com"; }, configureDeepLinking: x => { x.AddDefaultContentItemMapping(); })
.AddLti13PlatformWithDefaults(x => { x.Issuer = "https://mytest.com"; })
.AddDataService<DataService>();

builder.Services.RemoveAll<IHttpContextAccessor>();
Expand Down