Skip to content

Commit

Permalink
feat(breaking): Move front channel embeds to content (#862)
Browse files Browse the repository at this point in the history
<!--- Provide a general summary of your changes in the Title above -->

## Description

<!--- Describe your changes in detail -->

This moves the front channel embeds to the content, and adds a new type
`MainContentReference`.
`MainContentReference` must be an URL.

Added support for markdown in `AdditionalInfo`

## Related Issue(s)

- #856 

## Verification

- [x] **Your** code builds clean without any errors or warnings
- [x] Manual testing done (required)
- [ ] Relevant automated test added (if you find this hard, leave it and
we'll help out)

## Documentation

- [ ] Documentation is updated (either in `docs`-directory, Altinnpedia
or a separate linked PR in
[altinn-studio-docs.](https://github.com/Altinn/altinn-studio-docs), if
applicable)

---------
Co-authored-by: Knut Haug <knut.espen.haug@digdir.no>
Co-authored-by: Bjørn Dybvik Langfors <bdl@digdir.no>
  • Loading branch information
oskogstad authored Jun 19, 2024
1 parent d2482fb commit c9b50e9
Show file tree
Hide file tree
Showing 23 changed files with 1,656 additions and 119 deletions.
2 changes: 1 addition & 1 deletion docs/schema/V1/schema.verified.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ type AuthorizedParty {
type Content {
type: ContentType!
value: [Localization!]!
mediaType: String
}

type Dialog {
Expand Down Expand Up @@ -115,7 +116,6 @@ type Element {
type ElementUrl {
id: UUID!
url: URL!
mediaType: String
consumerType: ElementUrlConsumer!
}

Expand Down
49 changes: 26 additions & 23 deletions docs/schema/V1/swagger.verified.json
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,8 @@
"Value": "Some Title",
"CultureCode": "en-us"
}
]
],
"MediaType": null
},
{
"Type": 3,
Expand All @@ -238,7 +239,8 @@
"Value": "Some Summary",
"CultureCode": "en-us"
}
]
],
"MediaType": null
}
],
"SearchTags": [
Expand All @@ -252,7 +254,7 @@
"Elements": [
{
"Id": "02a72809-eddd-4192-864d-8f1755d72f4e",
"Type": "http://example.com/some-type",
"Type": "https://example.com/some-type",
"ExternalReference": null,
"AuthorizationAttribute": null,
"RelatedDialogElementId": null,
Expand All @@ -265,8 +267,7 @@
"Urls": [
{
"Id": "858177cb-8584-4d10-a086-3a5defa7a6c3",
"Url": "http://example.com/some-url",
"MediaType": "application/json",
"Url": "https://example.com/some-url",
"ConsumerType": 0
}
]
Expand Down Expand Up @@ -2301,6 +2302,10 @@
"items": {
"$ref": "#/components/schemas/LocalizationDto"
}
},
"mediaType": {
"type": "string",
"nullable": true
}
}
},
Expand All @@ -2312,14 +2317,16 @@
"SenderName",
"Summary",
"AdditionalInfo",
"ExtendedStatus"
"ExtendedStatus",
"MainContentReference"
],
"enum": [
"Title",
"SenderName",
"Summary",
"AdditionalInfo",
"ExtendedStatus"
"ExtendedStatus",
"MainContentReference"
]
},
"LocalizationDto": {
Expand Down Expand Up @@ -2397,10 +2404,6 @@
"type": "string",
"format": "uri"
},
"mediaType": {
"type": "string",
"nullable": true
},
"consumerType": {
"$ref": "#/components/schemas/DialogElementUrlConsumerType_Values"
}
Expand Down Expand Up @@ -2902,6 +2905,10 @@
"items": {
"$ref": "#/components/schemas/LocalizationDto"
}
},
"mediaType": {
"type": "string",
"nullable": true
}
}
},
Expand Down Expand Up @@ -2962,10 +2969,6 @@
"type": "string",
"format": "uri"
},
"mediaType": {
"type": "string",
"nullable": true
},
"consumerType": {
"$ref": "#/components/schemas/DialogElementUrlConsumerType_Values"
}
Expand Down Expand Up @@ -3250,6 +3253,10 @@
"items": {
"$ref": "#/components/schemas/LocalizationDto"
}
},
"mediaType": {
"type": "string",
"nullable": true
}
}
},
Expand Down Expand Up @@ -3311,10 +3318,6 @@
"type": "string",
"format": "uri"
},
"mediaType": {
"type": "string",
"nullable": true
},
"consumerType": {
"$ref": "#/components/schemas/DialogElementUrlConsumerType_Values"
}
Expand Down Expand Up @@ -3947,6 +3950,10 @@
"items": {
"$ref": "#/components/schemas/LocalizationDto"
}
},
"mediaType": {
"type": "string",
"nullable": true
}
}
},
Expand Down Expand Up @@ -4005,10 +4012,6 @@
"type": "string",
"format": "uri"
},
"mediaType": {
"type": "string",
"nullable": true
},
"consumerType": {
"$ref": "#/components/schemas/DialogElementUrlConsumerType_Values"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,35 @@
using Digdir.Domain.Dialogporten.Application.Features.V1.Common.Localizations;
using FluentValidation;
using HtmlAgilityPack;
using Markdig;

namespace Digdir.Domain.Dialogporten.Application.Common.Extensions.FluentValidation;

internal static class FluentValidationLocalizationDtoExtensions
{
private static readonly string[] AllowedTags = ["p", "a", "br", "em", "strong", "ul", "ol", "li"];
private static readonly string ContainsValidHtmlError =
$"{{PropertyName}} contains unsupported html. The following tags are supported: " +
"Value contains unsupported HTML/markdown. The following tags are supported: " +
$"[{string.Join(",", AllowedTags.Select(x => '<' + x + '>'))}]. Tag attributes " +
$"are not supported except for on '<a>' which must contain a 'href' starting " +
$"with 'https://'.";
"are not supported except for on '<a>' which must contain a 'href' starting " +
"with 'https://'.";

public static IRuleBuilderOptions<T, LocalizationDto> ContainsValidHtml<T>(this IRuleBuilder<T, LocalizationDto> ruleBuilder)
public static IRuleBuilderOptions<T, LocalizationDto> ContainsValidHtml<T>(
this IRuleBuilder<T, LocalizationDto> ruleBuilder)
{
return ruleBuilder
.Must(x => x.Value is null || x.Value.HtmlAgilityPackCheck())
.WithMessage(ContainsValidHtmlError);
}

public static IRuleBuilderOptions<T, LocalizationDto> ContainsValidMarkdown<T>(
this IRuleBuilder<T, LocalizationDto> ruleBuilder)
{
return ruleBuilder
.Must(x => x.Value is null || Markdown.ToHtml(x.Value).HtmlAgilityPackCheck())
.WithMessage(ContainsValidHtmlError);
}

private static bool HtmlAgilityPackCheck(this string html)
{
var doc = new HtmlDocument();
Expand All @@ -33,8 +43,8 @@ private static bool HtmlAgilityPackCheck(this string html)
{
return false;
}
// If the node is a hyperlink, it should only have an href attribute
// and it should start with 'https://'
// If the node is a hyperlink, it should only have a href attribute,
// and it must start with 'https://'
if (node.IsAnchorTag())
{
if (!node.IsValidAnchorTag())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<ItemGroup>
<PackageReference Include="AutoMapper" Version="13.0.1" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.61" />
<PackageReference Include="Markdig.Signed" Version="0.37.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.9.2" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ public sealed class GetDialogElementUrlDto
{
public Guid Id { get; set; }
public Uri Url { get; set; } = null!;
public string? MediaType { get; set; }

public DialogElementUrlConsumerType.Values ConsumerType { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public sealed class GetDialogContentDto
{
public DialogContentType.Values Type { get; set; }
public List<LocalizationDto> Value { get; set; } = [];
public string? MediaType { get; set; }
}

public sealed class GetDialogDialogActivityDto
Expand Down Expand Up @@ -130,7 +131,6 @@ public sealed class GetDialogDialogElementUrlDto
{
public Guid Id { get; set; }
public Uri Url { get; set; } = null!;
public string? MediaType { get; set; }

public DialogElementUrlConsumerType.Values ConsumerType { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ public sealed class GetDialogElementUrlDto
{
public Guid Id { get; set; }
public Uri Url { get; set; } = null!;
public string? MediaType { get; set; }

public DialogElementUrlConsumerType.Values ConsumerType { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,26 @@ public CreateDialogContentDtoValidator()
ClassLevelCascadeMode = CascadeMode.Stop;
RuleFor(x => x.Type)
.IsInEnum();
RuleFor(x => x.MediaType)
.Must((dto, value) =>
{
var type = DialogContentType.GetValue(dto.Type);
return value is null ? type.AllowedMediaTypes.Length == 0 : type.AllowedMediaTypes.Contains(value);
})
.WithMessage(x =>
$"{{PropertyName}} '{x.MediaType ?? "null"}' is not allowed for content type {DialogContentType.GetValue(x.Type).Name}. " +
$"Valid media types are: {(DialogContentType.GetValue(x.Type).AllowedMediaTypes.Length == 0 ? "None" :
$"{string.Join(", ", DialogContentType.GetValue(x.Type).AllowedMediaTypes!)}")}");
RuleForEach(x => x.Value)
.ContainsValidHtml()
.When(x => DialogContentType.GetValue(x.Type).RenderAsHtml);
.When(x => x.MediaType is not null && (x.MediaType == MediaTypes.Html));
RuleForEach(x => x.Value)
.ContainsValidMarkdown()
.When(x => x.MediaType is not null && x.MediaType == MediaTypes.Markdown);
RuleForEach(x => x.Value)
.Must(x => Uri.TryCreate(x.Value, UriKind.Absolute, out var uri) && uri.Scheme == Uri.UriSchemeHttps)
.When(x => x.MediaType is not null && x.MediaType.StartsWith(MediaTypes.EmbeddablePrefix, StringComparison.InvariantCultureIgnoreCase))
.WithMessage("{PropertyName} must be a valid HTTPS URL for embeddable content types");
RuleFor(x => x.Value)
.NotEmpty()
.SetValidator(x => new LocalizationDtosValidator(DialogContentType.GetValue(x.Type).MaxLength));
Expand Down Expand Up @@ -171,13 +188,6 @@ public CreateDialogDialogElementUrlDtoValidator()
.NotNull()
.IsValidUri()
.MaximumLength(Constants.DefaultMaxUriLength);
RuleFor(x => x.MediaType)
.MaximumLength(Constants.DefaultMaxStringLength);
RuleFor(x => x.MediaType)
.Must(MediaTypes.IsValid!)
.When(x => x.MediaType != null)
.WithMessage("Invalid media type, see docs for complete list <URL TDB>");

RuleFor(x => x.ConsumerType)
.IsInEnum();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public sealed class CreateDialogContentDto
{
public DialogContentType.Values Type { get; set; }
public List<LocalizationDto> Value { get; set; } = [];
public string? MediaType { get; set; }
}

public sealed class CreateDialogSearchTagDto
Expand Down Expand Up @@ -109,7 +110,6 @@ public sealed class CreateDialogDialogElementDto
public sealed class CreateDialogDialogElementUrlDto
{
public Uri Url { get; set; } = null!;
public string? MediaType { get; set; }

public DialogElementUrlConsumerType.Values ConsumerType { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,26 @@ public UpdateDialogContentDtoValidator()
ClassLevelCascadeMode = CascadeMode.Stop;
RuleFor(x => x.Type)
.IsInEnum();
RuleFor(x => x.MediaType)
.Must((dto, value) =>
{
var type = DialogContentType.GetValue(dto.Type);
return value is null ? type.AllowedMediaTypes.Length == 0 : type.AllowedMediaTypes.Contains(value);
})
.WithMessage(x =>
$"{{PropertyName}} '{x.MediaType ?? "null"}' is not allowed for content type {DialogContentType.GetValue(x.Type).Name}. " +
$"Valid media types are: {(DialogContentType.GetValue(x.Type).AllowedMediaTypes.Length == 0 ? "None" :
$"{string.Join(", ", DialogContentType.GetValue(x.Type).AllowedMediaTypes!)}")}");
RuleForEach(x => x.Value)
.ContainsValidHtml()
.When(x => DialogContentType.GetValue(x.Type).RenderAsHtml);
.When(x => x.MediaType is not null && (x.MediaType == MediaTypes.Html));
RuleForEach(x => x.Value)
.ContainsValidMarkdown()
.When(x => x.MediaType is not null && x.MediaType == MediaTypes.Markdown);
RuleForEach(x => x.Value)
.Must(x => Uri.TryCreate(x.Value, UriKind.Absolute, out var uri) && uri.Scheme == Uri.UriSchemeHttps)
.When(x => x.MediaType is not null && x.MediaType.StartsWith(MediaTypes.EmbeddablePrefix, StringComparison.InvariantCultureIgnoreCase))
.WithMessage("{PropertyName} must be a valid HTTPS URL for embeddable content types");
RuleFor(x => x.Value)
.NotEmpty()
.SetValidator(x => new LocalizationDtosValidator(DialogContentType.GetValue(x.Type).MaxLength));
Expand Down Expand Up @@ -168,12 +185,6 @@ public UpdateDialogDialogElementUrlDtoValidator()
.NotNull()
.IsValidUri()
.MaximumLength(Constants.DefaultMaxUriLength);
RuleFor(x => x.MediaType)
.MaximumLength(Constants.DefaultMaxStringLength);
RuleFor(x => x.MediaType)
.Must(MediaTypes.IsValid!)
.When(x => x.MediaType != null)
.WithMessage("Invalid media type, see docs for complete list <URL TDB>");
RuleFor(x => x.ConsumerType)
.IsInEnum();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public sealed class UpdateDialogContentDto
{
public DialogContentType.Values Type { get; set; }
public List<LocalizationDto> Value { get; set; } = [];
public string? MediaType { get; set; }
}

public sealed class UpdateDialogSearchTagDto
Expand Down Expand Up @@ -112,7 +113,6 @@ public sealed class UpdateDialogDialogElementUrlDto
{
public Guid? Id { get; set; }
public Uri Url { get; set; } = null!;
public string? MediaType { get; set; }

public DialogElementUrlConsumerType.Values ConsumerType { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public sealed class GetDialogContentDto
{
public DialogContentType.Values Type { get; set; }
public List<LocalizationDto> Value { get; set; } = [];
public string? MediaType { get; set; }
}

public sealed class GetDialogSearchTagDto
Expand Down Expand Up @@ -131,7 +132,6 @@ public sealed class GetDialogDialogElementUrlDto
{
public Guid Id { get; set; }
public Uri Url { get; set; } = null!;
public string? MediaType { get; set; }

public DialogElementUrlConsumerType.Values ConsumerType { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ public class DialogContent : IEntity
public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset UpdatedAt { get; set; }

public string? MediaType { get; set; }

// === Dependent relationships ===
public Guid DialogId { get; set; }
public DialogEntity Dialog { get; set; } = null!;
Expand Down
Loading

0 comments on commit c9b50e9

Please sign in to comment.