From 96209ed53dc0d6217ee2272a49f85a8b4cc2ba3b Mon Sep 17 00:00:00 2001 From: ninjapiratica Date: Wed, 30 Oct 2024 22:52:18 -0700 Subject: [PATCH] Bug fixes (#10) * rename IDepLinkingService to IDeepLinkingService * Update DeepLinkingService.HandleResponseAsync * make deep linking line item saving more robust * convert records from positional to properties * replace
in documentation * update example * test broken build * fix broken build --- NP.Lti13Platform.Core/README.md | 22 ++++----- NP.Lti13Platform.Core/Startup.cs | 13 +++++- .../DeepLinkSettingsOverride.cs | 14 +++++- .../Populators/DeepLinkingPopulator.cs | 2 +- NP.Lti13Platform.DeepLinking/README.md | 22 ++++----- .../Services/DeepLinkingService.cs | 4 +- .../Services/IDeepLinkingService.cs | 4 +- NP.Lti13Platform.DeepLinking/Startup.cs | 46 ++++++++++--------- .../Controllers/HomeController.cs | 26 ++++++++++- NP.Lti13Platform.WebExample/Program.cs | 2 +- 10 files changed, 101 insertions(+), 54 deletions(-) diff --git a/NP.Lti13Platform.Core/README.md b/NP.Lti13Platform.Core/README.md index d273908..fb2564e 100644 --- a/NP.Lti13Platform.Core/README.md +++ b/NP.Lti13Platform.Core/README.md @@ -96,43 +96,43 @@ builder.Services The platform information to identify the platform server, contacts, etc. -
+*** `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. -
+*** `ContactEmail` Administrative contact email for the platform instance. -
+*** `Description` Descriptive phrase for the platform instance. -
+*** `Name` Name for the platform instance. -
+*** `Url` Home HTTPS URL endpoint for the platform instance. -
+*** `ProductFamilyCode` Vendor product family code for the type of platform. -
+*** `Version` @@ -142,25 +142,25 @@ Vendor product version for the platform. The configuration for handling of tokens between the platform and the tools. -
+*** `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. -
+*** `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. -
+*** `MessageTokenExpirationSeconds` Default: `300` The expiration time of the lti messages that are sent to the tools. -
+*** `AccessTokenExpirationSeconds` Default: `3600` diff --git a/NP.Lti13Platform.Core/Startup.cs b/NP.Lti13Platform.Core/Startup.cs index 0e2c66a..c814fd8 100644 --- a/NP.Lti13Platform.Core/Startup.cs +++ b/NP.Lti13Platform.Core/Startup.cs @@ -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); - /// has the list of possible values. - public record LaunchPresentationOverride(string? DocumentTarget, double? Height, double? Width, string? ReturnUrl, string? Locale); + public record LaunchPresentationOverride + { + /// + /// has the list of possible values. + /// + 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; } + } } diff --git a/NP.Lti13Platform.DeepLinking/DeepLinkSettingsOverride.cs b/NP.Lti13Platform.DeepLinking/DeepLinkSettingsOverride.cs index eab22bc..06cc1ea 100644 --- a/NP.Lti13Platform.DeepLinking/DeepLinkSettingsOverride.cs +++ b/NP.Lti13Platform.DeepLinking/DeepLinkSettingsOverride.cs @@ -1,4 +1,16 @@ namespace NP.Lti13Platform.DeepLinking { - public record DeepLinkSettingsOverride(string? DeepLinkReturnUrl, IEnumerable? AcceptTypes, IEnumerable? AcceptPresentationDocumentTargets, IEnumerable? AcceptMediaTypes, bool? AcceptMultiple, bool? AcceptLineItem, bool? AutoCreate, string? Title, string? Text, string? Data); + public record DeepLinkSettingsOverride + { + public string? DeepLinkReturnUrl { get; set; } + public IEnumerable? AcceptTypes { get; set; } + public IEnumerable? AcceptPresentationDocumentTargets { get; set; } + public IEnumerable? 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; } + } } diff --git a/NP.Lti13Platform.DeepLinking/Populators/DeepLinkingPopulator.cs b/NP.Lti13Platform.DeepLinking/Populators/DeepLinkingPopulator.cs index 13d3f17..a287e6f 100644 --- a/NP.Lti13Platform.DeepLinking/Populators/DeepLinkingPopulator.cs +++ b/NP.Lti13Platform.DeepLinking/Populators/DeepLinkingPopulator.cs @@ -57,7 +57,7 @@ public class DeepLinkSettingsMessage } } - public class DeepLinkingPopulator(LinkGenerator linkGenerator, IDepLinkingService deepLinkingService) : Populator + public class DeepLinkingPopulator(LinkGenerator linkGenerator, IDeepLinkingService deepLinkingService) : Populator { public override async Task PopulateAsync(IDeepLinkingMessage obj, MessageScope scope, CancellationToken cancellationToken = default) { diff --git a/NP.Lti13Platform.DeepLinking/README.md b/NP.Lti13Platform.DeepLinking/README.md index 4e643ef..b7a1f9c 100644 --- a/NP.Lti13Platform.DeepLinking/README.md +++ b/NP.Lti13Platform.DeepLinking/README.md @@ -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 @@ -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. -
+*** `AcceptPresentationDocumentTargets` Default: `["embed", "iframe", "window"]`{:csharp} Defines how the content items will be shown to users (Embedded, Iframe, Window). -
+*** `AcceptTypes` Default: `["file", "html", "image", "link", "ltiResourceLink"]`{:csharp} Defines which types of content items the platform is looking for (File, Html, Image, Link, ResourceLink). -
+*** `AcceptMediaTypes` Default: `["image/*", "text/html"]`{:csharp} Defines which media types the platform is looking for (image/*, text/html). -
+*** `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. -
+*** `AcceptMultiple` Default: `true`{:csharp} Whether the platform allows multiple content items to be submitted in a single response. -
+*** `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. -
+*** `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. -
+*** `ContentItemTypes` Default: `[]`{:csharp} diff --git a/NP.Lti13Platform.DeepLinking/Services/DeepLinkingService.cs b/NP.Lti13Platform.DeepLinking/Services/DeepLinkingService.cs index c372514..409b0ae 100644 --- a/NP.Lti13Platform.DeepLinking/Services/DeepLinkingService.cs +++ b/NP.Lti13Platform.DeepLinking/Services/DeepLinkingService.cs @@ -4,9 +4,9 @@ namespace NP.Lti13Platform.DeepLinking.Services { - internal class DeepLinkingService(IOptionsMonitor config, IHttpContextAccessor httpContextAccessor) : IDepLinkingService + internal class DeepLinkingService(IOptionsMonitor config, IHttpContextAccessor httpContextAccessor) : IDeepLinkingService { - public Task HandleResponseAsync(DeepLinkResponse response, CancellationToken cancellationToken = default) => Task.FromResult(Results.Ok(response)); + public Task HandleResponseAsync(string clientId, string deploymentId, string? contextId, DeepLinkResponse response, CancellationToken cancellationToken = default) => Task.FromResult(Results.Ok(response)); public async Task GetConfigAsync(string clientId, CancellationToken cancellationToken = default) { diff --git a/NP.Lti13Platform.DeepLinking/Services/IDeepLinkingService.cs b/NP.Lti13Platform.DeepLinking/Services/IDeepLinkingService.cs index 10a4a49..96170d6 100644 --- a/NP.Lti13Platform.DeepLinking/Services/IDeepLinkingService.cs +++ b/NP.Lti13Platform.DeepLinking/Services/IDeepLinkingService.cs @@ -3,9 +3,9 @@ namespace NP.Lti13Platform.DeepLinking.Services { - public interface IDepLinkingService + public interface IDeepLinkingService { - Task HandleResponseAsync(DeepLinkResponse response, CancellationToken cancellationToken = default); + Task HandleResponseAsync(string clientId, string deploymentId, string? contextId, DeepLinkResponse response, CancellationToken cancellationToken = default); Task GetConfigAsync(string clientId, CancellationToken cancellationToken = default); } diff --git a/NP.Lti13Platform.DeepLinking/Startup.cs b/NP.Lti13Platform.DeepLinking/Startup.cs index 7918bae..58e06c6 100644 --- a/NP.Lti13Platform.DeepLinking/Startup.cs +++ b/NP.Lti13Platform.DeepLinking/Startup.cs @@ -37,7 +37,7 @@ public static Lti13PlatformBuilder AddDefaultDeepLinkingService(this Lti13Platfo configure ??= x => { }; builder.Services.Configure(configure); - builder.Services.AddTransient(); + builder.Services.AddTransient(); return builder; } @@ -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 logger, ITokenService tokenService, ICoreDataService coreDataService, IDeepLinkingDataService deepLinkingDataService, IDepLinkingService deepLinkingService, CancellationToken cancellationToken) => + _ = app.MapPost(config.DeepLinkingResponseUrl, + async ([FromForm] DeepLinkResponseRequest request, string? contextId, ILogger 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"; @@ -115,6 +115,16 @@ 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(x.Value) : null); + }) + .ToList(); + var response = new DeepLinkResponse { Data = validatedToken.ClaimsIdentity.FindFirst("https://purl.imsglobal.org/spec/lti-dl/claim/data")?.Value, @@ -122,13 +132,7 @@ public static IEndpointRouteBuilder UseLti13PlatformDeepLinking(this IEndpointRo 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)) @@ -143,25 +147,25 @@ 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 }); } }); @@ -169,7 +173,7 @@ await deepLinkingDataService.SaveLineItemAsync(new LineItem 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(); diff --git a/NP.Lti13Platform.WebExample/Controllers/HomeController.cs b/NP.Lti13Platform.WebExample/Controllers/HomeController.cs index e120b48..244ace1 100644 --- a/NP.Lti13Platform.WebExample/Controllers/HomeController.cs +++ b/NP.Lti13Platform.WebExample/Controllers/HomeController.cs @@ -23,9 +23,31 @@ public async Task 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) }); } diff --git a/NP.Lti13Platform.WebExample/Program.cs b/NP.Lti13Platform.WebExample/Program.cs index 6e005c9..db2c3a1 100644 --- a/NP.Lti13Platform.WebExample/Program.cs +++ b/NP.Lti13Platform.WebExample/Program.cs @@ -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(); builder.Services.RemoveAll();