diff --git a/source/nodejs/.prettierrc.json b/source/nodejs/.prettierrc.json index 650e4d388c..7bb34f814c 100644 --- a/source/nodejs/.prettierrc.json +++ b/source/nodejs/.prettierrc.json @@ -1,4 +1,5 @@ { + "tabWidth": 4, "printWidth": 100, "trailingComma": "none", "quoteProps": "preserve" diff --git a/source/nodejs/ac-typed-schema/src/markdown/languages/de.json b/source/nodejs/ac-typed-schema/src/markdown/languages/de.json index d2b1fbea67..93c63ab074 100644 --- a/source/nodejs/ac-typed-schema/src/markdown/languages/de.json +++ b/source/nodejs/ac-typed-schema/src/markdown/languages/de.json @@ -232,5 +232,16 @@ "Style hint for `TableCell`.": "Style hint for `TableCell`.", "Allows users to filter choices in a choice set.": "Allows users to filter choices in a choice set.", "URL of an image to display before playing. Supports data URI in version 1.2+. If poster is omitted, the Media element will either use a default poster (controlled by the host application) ot will attempt to automatically pull the poster from the target video service when the source URL points to a video from a Web provider such as YouTube.": "URL of an image to display before playing. Supports data URI in version 1.2+. If poster is omitted, the Media element will either use a default poster (controlled by the host application) ot will attempt to automatically pull the poster from the target video service when the source URL points to a video from a Web provider such as YouTube.", - "Mime type of associated media (e.g. `\"video/mp4\"`). For YouTube and other Web video URLs, mimeType can be omitted.": "Mime type of associated media (e.g. `\"video/mp4\"`). For YouTube and other Web video URLs, mimeType can be omitted." + "Mime type of associated media (e.g. `\"video/mp4\"`). For YouTube and other Web video URLs, mimeType can be omitted.": "Mime type of associated media (e.g. `\"video/mp4\"`). For YouTube and other Web video URLs, mimeType can be omitted.", + "Defines a source for captions": "Defines a source for captions", + "Defines various metadata properties": "Defines various metadata properties", + "Defines various metadata properties typically not used for rendering the card": "Defines various metadata properties typically not used for rendering the card", + "URL of an image to display before playing. Supports data URI in version 1.2+. If poster is omitted, the Media element will either use a default poster (controlled by the host application) or will attempt to automatically pull the poster from the target video service when the source URL points to a video from a Web provider such as YouTube.": "URL of an image to display before playing. Supports data URI in version 1.2+. If poster is omitted, the Media element will either use a default poster (controlled by the host application) or will attempt to automatically pull the poster from the target video service when the source URL points to a video from a Web provider such as YouTube.", + "Array of captions sources for the media element to provide.": "Array of captions sources for the media element to provide.", + "Mime type of associated media (e.g. `\"video/mp4\"`). For YouTube and other Web video URLs, `mimeType` can be omitted.": "Mime type of associated media (e.g. `\"video/mp4\"`). For YouTube and other Web video URLs, `mimeType` can be omitted.", + "Mime type of associated caption file (e.g. `\"vtt\"`). For rendering in JavaScript, only `\"vtt\"` is supported, for rendering in UWP, `\"vtt\"` and `\"srt\"` are supported.": "Mime type of associated caption file (e.g. `\"vtt\"`). For rendering in JavaScript, only `\"vtt\"` is supported, for rendering in UWP, `\"vtt\"` and `\"srt\"` are supported.", + "URL to captions.": "URL to captions.", + "Label of this caption to show to the user.": "Label of this caption to show to the user.", + "A timestamp that informs a Host when the card content has expired, and that it should trigger a refresh as appropriate. The format is ISO-8601 Instant format. E.g., 2022-01-01T12:00:00Z": "A timestamp that informs a Host when the card content has expired, and that it should trigger a refresh as appropriate. The format is ISO-8601 Instant format. E.g., 2022-01-01T12:00:00Z", + "URL that uniquely identifies the card and serves as a browser fallback that can be used by some hosts.": "URL that uniquely identifies the card and serves as a browser fallback that can be used by some hosts." } \ No newline at end of file diff --git a/source/nodejs/ac-typed-schema/src/markdown/languages/en.json b/source/nodejs/ac-typed-schema/src/markdown/languages/en.json index d2b1fbea67..93c63ab074 100644 --- a/source/nodejs/ac-typed-schema/src/markdown/languages/en.json +++ b/source/nodejs/ac-typed-schema/src/markdown/languages/en.json @@ -232,5 +232,16 @@ "Style hint for `TableCell`.": "Style hint for `TableCell`.", "Allows users to filter choices in a choice set.": "Allows users to filter choices in a choice set.", "URL of an image to display before playing. Supports data URI in version 1.2+. If poster is omitted, the Media element will either use a default poster (controlled by the host application) ot will attempt to automatically pull the poster from the target video service when the source URL points to a video from a Web provider such as YouTube.": "URL of an image to display before playing. Supports data URI in version 1.2+. If poster is omitted, the Media element will either use a default poster (controlled by the host application) ot will attempt to automatically pull the poster from the target video service when the source URL points to a video from a Web provider such as YouTube.", - "Mime type of associated media (e.g. `\"video/mp4\"`). For YouTube and other Web video URLs, mimeType can be omitted.": "Mime type of associated media (e.g. `\"video/mp4\"`). For YouTube and other Web video URLs, mimeType can be omitted." + "Mime type of associated media (e.g. `\"video/mp4\"`). For YouTube and other Web video URLs, mimeType can be omitted.": "Mime type of associated media (e.g. `\"video/mp4\"`). For YouTube and other Web video URLs, mimeType can be omitted.", + "Defines a source for captions": "Defines a source for captions", + "Defines various metadata properties": "Defines various metadata properties", + "Defines various metadata properties typically not used for rendering the card": "Defines various metadata properties typically not used for rendering the card", + "URL of an image to display before playing. Supports data URI in version 1.2+. If poster is omitted, the Media element will either use a default poster (controlled by the host application) or will attempt to automatically pull the poster from the target video service when the source URL points to a video from a Web provider such as YouTube.": "URL of an image to display before playing. Supports data URI in version 1.2+. If poster is omitted, the Media element will either use a default poster (controlled by the host application) or will attempt to automatically pull the poster from the target video service when the source URL points to a video from a Web provider such as YouTube.", + "Array of captions sources for the media element to provide.": "Array of captions sources for the media element to provide.", + "Mime type of associated media (e.g. `\"video/mp4\"`). For YouTube and other Web video URLs, `mimeType` can be omitted.": "Mime type of associated media (e.g. `\"video/mp4\"`). For YouTube and other Web video URLs, `mimeType` can be omitted.", + "Mime type of associated caption file (e.g. `\"vtt\"`). For rendering in JavaScript, only `\"vtt\"` is supported, for rendering in UWP, `\"vtt\"` and `\"srt\"` are supported.": "Mime type of associated caption file (e.g. `\"vtt\"`). For rendering in JavaScript, only `\"vtt\"` is supported, for rendering in UWP, `\"vtt\"` and `\"srt\"` are supported.", + "URL to captions.": "URL to captions.", + "Label of this caption to show to the user.": "Label of this caption to show to the user.", + "A timestamp that informs a Host when the card content has expired, and that it should trigger a refresh as appropriate. The format is ISO-8601 Instant format. E.g., 2022-01-01T12:00:00Z": "A timestamp that informs a Host when the card content has expired, and that it should trigger a refresh as appropriate. The format is ISO-8601 Instant format. E.g., 2022-01-01T12:00:00Z", + "URL that uniquely identifies the card and serves as a browser fallback that can be used by some hosts.": "URL that uniquely identifies the card and serves as a browser fallback that can be used by some hosts." } \ No newline at end of file diff --git a/source/nodejs/ac-typed-schema/src/markdown/languages/sp.json b/source/nodejs/ac-typed-schema/src/markdown/languages/sp.json index d2b1fbea67..93c63ab074 100644 --- a/source/nodejs/ac-typed-schema/src/markdown/languages/sp.json +++ b/source/nodejs/ac-typed-schema/src/markdown/languages/sp.json @@ -232,5 +232,16 @@ "Style hint for `TableCell`.": "Style hint for `TableCell`.", "Allows users to filter choices in a choice set.": "Allows users to filter choices in a choice set.", "URL of an image to display before playing. Supports data URI in version 1.2+. If poster is omitted, the Media element will either use a default poster (controlled by the host application) ot will attempt to automatically pull the poster from the target video service when the source URL points to a video from a Web provider such as YouTube.": "URL of an image to display before playing. Supports data URI in version 1.2+. If poster is omitted, the Media element will either use a default poster (controlled by the host application) ot will attempt to automatically pull the poster from the target video service when the source URL points to a video from a Web provider such as YouTube.", - "Mime type of associated media (e.g. `\"video/mp4\"`). For YouTube and other Web video URLs, mimeType can be omitted.": "Mime type of associated media (e.g. `\"video/mp4\"`). For YouTube and other Web video URLs, mimeType can be omitted." + "Mime type of associated media (e.g. `\"video/mp4\"`). For YouTube and other Web video URLs, mimeType can be omitted.": "Mime type of associated media (e.g. `\"video/mp4\"`). For YouTube and other Web video URLs, mimeType can be omitted.", + "Defines a source for captions": "Defines a source for captions", + "Defines various metadata properties": "Defines various metadata properties", + "Defines various metadata properties typically not used for rendering the card": "Defines various metadata properties typically not used for rendering the card", + "URL of an image to display before playing. Supports data URI in version 1.2+. If poster is omitted, the Media element will either use a default poster (controlled by the host application) or will attempt to automatically pull the poster from the target video service when the source URL points to a video from a Web provider such as YouTube.": "URL of an image to display before playing. Supports data URI in version 1.2+. If poster is omitted, the Media element will either use a default poster (controlled by the host application) or will attempt to automatically pull the poster from the target video service when the source URL points to a video from a Web provider such as YouTube.", + "Array of captions sources for the media element to provide.": "Array of captions sources for the media element to provide.", + "Mime type of associated media (e.g. `\"video/mp4\"`). For YouTube and other Web video URLs, `mimeType` can be omitted.": "Mime type of associated media (e.g. `\"video/mp4\"`). For YouTube and other Web video URLs, `mimeType` can be omitted.", + "Mime type of associated caption file (e.g. `\"vtt\"`). For rendering in JavaScript, only `\"vtt\"` is supported, for rendering in UWP, `\"vtt\"` and `\"srt\"` are supported.": "Mime type of associated caption file (e.g. `\"vtt\"`). For rendering in JavaScript, only `\"vtt\"` is supported, for rendering in UWP, `\"vtt\"` and `\"srt\"` are supported.", + "URL to captions.": "URL to captions.", + "Label of this caption to show to the user.": "Label of this caption to show to the user.", + "A timestamp that informs a Host when the card content has expired, and that it should trigger a refresh as appropriate. The format is ISO-8601 Instant format. E.g., 2022-01-01T12:00:00Z": "A timestamp that informs a Host when the card content has expired, and that it should trigger a refresh as appropriate. The format is ISO-8601 Instant format. E.g., 2022-01-01T12:00:00Z", + "URL that uniquely identifies the card and serves as a browser fallback that can be used by some hosts.": "URL that uniquely identifies the card and serves as a browser fallback that can be used by some hosts." } \ No newline at end of file diff --git a/source/nodejs/adaptivecards-controls/src/calendar.ts b/source/nodejs/adaptivecards-controls/src/calendar.ts index d149f5fec9..1337f81bdf 100644 --- a/source/nodejs/adaptivecards-controls/src/calendar.ts +++ b/source/nodejs/adaptivecards-controls/src/calendar.ts @@ -162,7 +162,7 @@ export class Calendar extends InputControl { var month = newDate.getMonth(); - this._miniCalendarElement.innerHTML = ""; + Utils.clearElement(this._miniCalendarElement); this._miniCalendarElement.classList.remove( "ms-ctrl-slide", @@ -282,7 +282,7 @@ export class Calendar extends InputControl { attach(rootElement: HTMLElement) { super.attach(rootElement); - rootElement.innerHTML = ""; + Utils.clearElement(rootElement); rootElement.appendChild(this._rootContainerElement); } diff --git a/source/nodejs/adaptivecards-controls/src/checkbox.ts b/source/nodejs/adaptivecards-controls/src/checkbox.ts index 40561684c9..d1dcd75312 100644 --- a/source/nodejs/adaptivecards-controls/src/checkbox.ts +++ b/source/nodejs/adaptivecards-controls/src/checkbox.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import { Constants } from "./constants"; import { InputControl } from "./inputcontrol"; +import { clearElement } from "./utils"; export class CheckBox extends InputControl { // Used to generate unique Ids @@ -69,7 +70,7 @@ export class CheckBox extends InputControl { labelElement.appendChild(this._spanElement); - rootElement.innerHTML = ""; + clearElement(rootElement); rootElement.appendChild(this._checkboxElement); rootElement.appendChild(labelElement); } diff --git a/source/nodejs/adaptivecards-controls/src/inputwithpopup.ts b/source/nodejs/adaptivecards-controls/src/inputwithpopup.ts index 56436a2ac0..ecea04ac0f 100644 --- a/source/nodejs/adaptivecards-controls/src/inputwithpopup.ts +++ b/source/nodejs/adaptivecards-controls/src/inputwithpopup.ts @@ -192,7 +192,7 @@ export abstract class InputWithPopup private updateLabel() { if (this._labelElement) { if (this._value) { - this._labelElement.innerHTML = this.getValueAsString(); + this._labelElement.innerText = this.getValueAsString(); this._labelElement.classList.remove("placeholder"); } else { diff --git a/source/nodejs/adaptivecards-controls/src/radiobutton.ts b/source/nodejs/adaptivecards-controls/src/radiobutton.ts index 7591b4fe56..a27ead2fa4 100644 --- a/source/nodejs/adaptivecards-controls/src/radiobutton.ts +++ b/source/nodejs/adaptivecards-controls/src/radiobutton.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import { Constants } from "./constants"; import { InputControl } from "./inputcontrol"; +import { clearElement } from "./utils"; export class RadioButton extends InputControl { // Used to generate unique Ids @@ -75,7 +76,7 @@ export class RadioButton extends InputControl { labelElement.appendChild(this._spanElement); - rootElement.innerHTML = ""; + clearElement(rootElement); rootElement.appendChild(this._checkboxElement); rootElement.appendChild(labelElement); } diff --git a/source/nodejs/adaptivecards-controls/src/textbox.ts b/source/nodejs/adaptivecards-controls/src/textbox.ts index 3c5d73765b..1e45597092 100644 --- a/source/nodejs/adaptivecards-controls/src/textbox.ts +++ b/source/nodejs/adaptivecards-controls/src/textbox.ts @@ -147,7 +147,7 @@ export class TextBox extends InputControl { this._editBox.placeholder = this._placeholder; this._editBox.onChange = () => { this.editBoxChanged(); } - this.rootElement.innerHTML = ""; + Utils.clearElement(this.rootElement); this.rootElement.appendChild(this._editBox.element); } diff --git a/source/nodejs/adaptivecards-controls/src/utils.ts b/source/nodejs/adaptivecards-controls/src/utils.ts index 7cfcf622ed..e1b6e9a23b 100644 --- a/source/nodejs/adaptivecards-controls/src/utils.ts +++ b/source/nodejs/adaptivecards-controls/src/utils.ts @@ -138,3 +138,8 @@ export function getAttributeValueAsInt(element: HTMLElement, attributeName: stri return defaultValue; } + +export function clearElement(element: HTMLElement) : void { + const trustedHtml = window.trustedTypes?.emptyHTML ?? ""; + element.innerHTML = trustedHtml as string; +} \ No newline at end of file diff --git a/source/nodejs/adaptivecards-react-testapp/package-lock.json b/source/nodejs/adaptivecards-react-testapp/package-lock.json index f9e78f6d93..2c9798325c 100644 --- a/source/nodejs/adaptivecards-react-testapp/package-lock.json +++ b/source/nodejs/adaptivecards-react-testapp/package-lock.json @@ -21,8 +21,7 @@ "react-scripts": "5.0.0", "typescript": "^4.1.2", "web-vitals": "^1.0.1" - }, - "devDependencies": {} + } }, "node_modules/@babel/code-frame": { "version": "7.16.7", diff --git a/source/nodejs/adaptivecards-react/src/adaptive-card.tsx b/source/nodejs/adaptivecards-react/src/adaptive-card.tsx index dc2fa36805..867fc661e6 100644 --- a/source/nodejs/adaptivecards-react/src/adaptive-card.tsx +++ b/source/nodejs/adaptivecards-react/src/adaptive-card.tsx @@ -124,7 +124,8 @@ export const AdaptiveCard = ({ try { card.parse(payload); const result = card.render() as HTMLElement; - targetRef.current.innerHTML = ''; + const trustedHtml = window.trustedTypes?.emptyHTML ?? ""; + targetRef.current.innerHTML = trustedHtml as string; targetRef.current.appendChild(result); } catch (cardRenderError) { if (onError) { diff --git a/source/nodejs/adaptivecards-ui-testapp/src/rendering-utils.ts b/source/nodejs/adaptivecards-ui-testapp/src/rendering-utils.ts index 4e0768b2ab..9d60a7ca51 100644 --- a/source/nodejs/adaptivecards-ui-testapp/src/rendering-utils.ts +++ b/source/nodejs/adaptivecards-ui-testapp/src/rendering-utils.ts @@ -4,6 +4,10 @@ import { getTestCasesList } from "./file-retriever-utils"; import { Action, AdaptiveCard, ExecuteAction, HostConfig, IMarkdownProcessingResult, Input, OpenUrlAction, PropertyBag, SerializationContext, SubmitAction, Version, Versions } from "adaptivecards"; import * as Remarkable from "remarkable"; +const ttPolicy = window.trustedTypes?.createPolicy('adaptivecards-ui-testapp', { + createHTML: value => value, +}); + export function listAllFiles(): HTMLLIElement[] { const testCasesList: HTMLLIElement[] = []; @@ -105,7 +109,9 @@ export function renderCard(cardJson: any, callbackFunction: Function): void { } const retrievedInputsDiv: HTMLElement = document.getElementById("retrievedInputsDiv"); - retrievedInputsDiv.innerHTML = inputsAsJson; + + const trustedHtml = ttPolicy?.createHTML(inputsAsJson) ?? inputsAsJson; + retrievedInputsDiv.innerHTML = trustedHtml as string; retrievedInputsDiv.style.visibility = "visible"; }; @@ -129,7 +135,8 @@ export function renderCard(cardJson: any, callbackFunction: Function): void { export function cardRenderedCallback(renderedCard: HTMLElement) { const renderedCardDiv = document.getElementById("renderedCardSpace"); - renderedCardDiv.innerHTML = ""; + const trustedHtml = window.trustedTypes?.emptyHTML ?? ""; + renderedCardDiv.innerHTML = trustedHtml as string; renderedCardDiv.appendChild(renderedCard); renderedCardDiv.style.visibility = "visible"; } diff --git a/source/nodejs/adaptivecards/src/card-elements.ts b/source/nodejs/adaptivecards/src/card-elements.ts index 5ad8cd4f9b..950833c340 100644 --- a/source/nodejs/adaptivecards/src/card-elements.ts +++ b/source/nodejs/adaptivecards/src/card-elements.ts @@ -12,7 +12,7 @@ import { StringWithSubstitutions, ContentTypes, IInput, - IResourceInformation, + IResourceInformation } from "./shared"; import * as Utils from "./utils"; import { @@ -51,6 +51,11 @@ import { CardObjectRegistry, GlobalRegistry, ElementSingletonBehavior } from "./ import { Strings } from "./strings"; import { MenuItem, PopupMenu } from "./controls"; +function clearElement(element: HTMLElement) : void { + const trustedHtml = window.trustedTypes?.emptyHTML ?? ""; + element.innerHTML = trustedHtml as string; +} + export function renderSeparation( hostConfig: HostConfig, separationDefinition: ISeparationDefinition, @@ -690,7 +695,7 @@ export abstract class CardElement extends CardObject { for (let i = 0; i < this.getActionCount(); i++) { let action = this.getActionAt(i); - + if (action) { result.push(action); } @@ -1073,7 +1078,8 @@ export class TextBlock extends BaseTextBlock { this._computedLineHeight * this.maxLines + "px"; } - this.renderedElement.innerHTML = this._originalInnerHtml; + const originalHtml = TextBlock._ttRoundtripPolicy?.createHTML(this._originalInnerHtml) ?? this._originalInnerHtml; + this.renderedElement.innerHTML = originalHtml as string; } } @@ -1086,12 +1092,12 @@ export class TextBlock extends BaseTextBlock { const isTextOnly = !children.length; const truncationSupported = isTextOnly || - (children.length === 1 && (children[0]).tagName.toLowerCase() === "p"); + (children.length === 1 && (children[0]).tagName.toLowerCase() === "p" && !(children[0]).children.length); if (truncationSupported) { const element = isTextOnly ? this.renderedElement : children[0]; - Utils.truncate(element, maxHeight, this._computedLineHeight); + Utils.truncateText(element, maxHeight, this._computedLineHeight); return true; } @@ -1100,6 +1106,22 @@ export class TextBlock extends BaseTextBlock { return false; } + // Markdown processing is handled outside of Adaptive Cards. It's up to the host to ensure that markdown is safely + // processed. + private static readonly _ttMarkdownPolicy = window.trustedTypes?.createPolicy( + "markdownPassthroughPolicy", + { createHTML: (value) => value } + ); + + // When "advanced" truncation is enabled (see GlobalSettings.useAdvancedCardBottomTruncation and + // GlobalSettings.useAdvancedTextBlockTruncation), we store the original pre-truncation content in + // _originalInnerHtml so that we can restore/recalculate truncation later if space availability has changed (see + // TextBlock.restoreOriginalContent()) + private static readonly _ttRoundtripPolicy = window.trustedTypes?.createPolicy( + "restoreContentsPolicy", + { createHTML: (value) => value } + ); + protected setText(value: string) { super.setText(value); @@ -1220,7 +1242,10 @@ export class TextBlock extends BaseTextBlock { if (this._treatAsPlainText) { element.innerText = this._processedText; } else { - element.innerHTML = this._processedText; + const processedHtml = + TextBlock._ttMarkdownPolicy?.createHTML(this._processedText) ?? + this._processedText; + element.innerHTML = processedHtml as string; } if (element.firstElementChild instanceof HTMLElement) { @@ -2018,7 +2043,7 @@ export class Image extends CardElement { if (this.renderedElement) { const card = this.getRootElement() as AdaptiveCard; - this.renderedElement.innerHTML = ""; + this.renderedElement; if (card && card.designMode) { const errorElement = document.createElement("div"); @@ -2527,7 +2552,7 @@ export class CaptionSource extends ContentSource { //#endregion - constructor(url?: string, mimeType?: string, label?:string) { + constructor(url?: string, mimeType?: string, label?: string) { super(url, mimeType); this.label = label; @@ -2624,7 +2649,7 @@ export class HTML5MediaPlayer extends MediaPlayer { this._captionSources.push(...this.owner.captionSources); } - static readonly supportedMediaTypes = [ "audio", "video" ]; + static readonly supportedMediaTypes = ["audio", "video"]; constructor(readonly owner: Media) { super(); @@ -2738,9 +2763,10 @@ export abstract class IFrameMediaMediaPlayer extends CustomMediaPlayer { iFrame.title = this.iFrameTitle; } - iFrame.allow = "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"; + iFrame.allow = + "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"; iFrame.allowFullscreen = true; - + container.appendChild(iFrame); return container; @@ -2783,7 +2809,7 @@ export class DailymotionPlayer extends IFrameMediaMediaPlayer { } getEmbedVideoUrl(): string { - return `https://www.dailymotion.com/embed/video/${this.videoId}?autoplay=1`; + return `https://www.dailymotion.com/embed/video/${this.videoId}?autoplay=1`; } } @@ -2799,7 +2825,9 @@ export class YouTubePlayer extends IFrameMediaMediaPlayer { } async fetchVideoDetails(): Promise { - this.posterUrl = this.videoId ? `https://img.youtube.com/vi/${this.videoId}/maxresdefault.jpg` : undefined; + this.posterUrl = this.videoId + ? `https://img.youtube.com/vi/${this.videoId}/maxresdefault.jpg` + : undefined; } getEmbedVideoUrl(): string { @@ -2815,22 +2843,28 @@ export class YouTubePlayer extends IFrameMediaMediaPlayer { export interface ICustomMediaPlayer { urlPatterns: RegExp[]; - createMediaPlayer: (matches: RegExpExecArray) => CustomMediaPlayer + createMediaPlayer: (matches: RegExpExecArray) => CustomMediaPlayer; } export class Media extends CardElement { static customMediaPlayers: ICustomMediaPlayer[] = [ { - urlPatterns: [ /^(?:https?:\/\/)?(?:www.)?youtube.com\/watch\?(?=.*v=([\w\d-_]+))(?=(?:.*t=(\d+))?).*/ig, /^(?:https?:\/\/)?youtu.be\/([\w\d-_]+)(?:\?t=(\d+))?/ig ], - createMediaPlayer: (matches) => new YouTubePlayer(matches, Strings.defaults.youTubeVideoPlayer()) + urlPatterns: [ + /^(?:https?:\/\/)?(?:www.)?youtube.com\/watch\?(?=.*v=([\w\d-_]+))(?=(?:.*t=(\d+))?).*/gi, + /^(?:https?:\/\/)?youtu.be\/([\w\d-_]+)(?:\?t=(\d+))?/gi + ], + createMediaPlayer: (matches) => + new YouTubePlayer(matches, Strings.defaults.youTubeVideoPlayer()) }, { - urlPatterns: [ /^(?:https?:\/\/)?vimeo.com\/([\w\d-_]+).*/ig ], - createMediaPlayer: (matches) => new VimeoPlayer(matches, Strings.defaults.vimeoVideoPlayer()) + urlPatterns: [/^(?:https?:\/\/)?vimeo.com\/([\w\d-_]+).*/gi], + createMediaPlayer: (matches) => + new VimeoPlayer(matches, Strings.defaults.vimeoVideoPlayer()) }, { - urlPatterns: [ /^(?:https?:\/\/)?(?:www.)?dailymotion.com\/video\/([\w\d-_]+).*/ig ], - createMediaPlayer: (matches) => new DailymotionPlayer(matches, Strings.defaults.dailymotionVideoPlayer()) + urlPatterns: [/^(?:https?:\/\/)?(?:www.)?dailymotion.com\/video\/([\w\d-_]+).*/gi], + createMediaPlayer: (matches) => + new DailymotionPlayer(matches, Strings.defaults.dailymotionVideoPlayer()) } ]; @@ -2873,7 +2907,7 @@ export class Media extends CardElement { let matches = pattern.exec(source.url); if (matches !== null) { - return provider.createMediaPlayer(matches); + return provider.createMediaPlayer(matches); } } } @@ -2890,8 +2924,7 @@ export class Media extends CardElement { if (this.renderedElement) { const mediaPlayerElement = this._mediaPlayer.render(); - - this.renderedElement.innerHTML = ""; + clearElement(this.renderedElement); this.renderedElement.appendChild(mediaPlayerElement); this._mediaPlayer.play(); @@ -3009,7 +3042,7 @@ export class Media extends CardElement { posterRootElement.appendChild(playButtonContainer); } - this.renderedElement.innerHTML = ""; + clearElement(this.renderedElement); this.renderedElement.appendChild(posterRootElement); } } @@ -3757,7 +3790,9 @@ export class ToggleInput extends Input { } isDirty(): boolean { - return this._checkboxInputElement ? this._checkboxInputElement.checked !== this._oldCheckboxValue : false; + return this._checkboxInputElement + ? this._checkboxInputElement.checked !== this._oldCheckboxValue + : false; } get value(): string | undefined { @@ -4276,7 +4311,7 @@ export class NumberInput extends Input { this._numberInputElement.style.width = "100%"; this._numberInputElement.tabIndex = this.isDesignMode() ? -1 : 0; - + if (this.defaultValue !== undefined) { this._numberInputElement.valueAsNumber = this.defaultValue; } @@ -4681,7 +4716,7 @@ export abstract class Action extends CardObject { isDesignMode(): boolean { const rootElement = this.getRootObject(); - return (rootElement instanceof CardElement) && rootElement.isDesignMode(); + return rootElement instanceof CardElement && rootElement.isDesignMode(); } protected updateCssClasses() { @@ -4808,15 +4843,16 @@ export abstract class Action extends CardObject { if (this.title) { element.setAttribute("aria-label", this.title); element.title = this.title; - } - else { + } else { element.removeAttribute("aria-label"); element.removeAttribute("title"); } if (this.tooltip) { const targetAriaAttribute = promoteTooltipToLabel - ? this.title ? "aria-description" : "aria-label" + ? this.title + ? "aria-description" + : "aria-label" : "aria-description"; element.setAttribute(targetAriaAttribute, this.tooltip); @@ -4887,7 +4923,7 @@ export abstract class Action extends CardObject { } getAllActions(): Action[] { - return [ this ]; + return [this]; } getResourceInformation(): IResourceInformation[] { @@ -4997,17 +5033,21 @@ export abstract class SubmitActionBase extends Action { context.serializeValue(target, prop.name, value); } ); - static readonly disabledUnlessAssociatedInputsChangeProperty = new BoolProperty(Versions.v1_6, "disabledUnlessAssociatedInputsChange", false); + static readonly disabledUnlessAssociatedInputsChangeProperty = new BoolProperty( + Versions.v1_6, + "disabledUnlessAssociatedInputsChange", + false + ); @property(SubmitActionBase.dataProperty) private _originalData?: PropertyBag; @property(SubmitActionBase.associatedInputsProperty) associatedInputs?: "auto" | "none"; - + @property(SubmitActionBase.disabledUnlessAssociatedInputsChangeProperty) disabledUnlessAssociatedInputsChange: boolean = false; - + //#endregion private _isPrepared: boolean = false; @@ -5104,8 +5144,10 @@ export abstract class SubmitActionBase extends Action { isEffectivelyEnabled(): boolean { let result = super.isEffectivelyEnabled(); - - return this.disabledUnlessAssociatedInputsChange ? result && this._areReferencedInputsDirty : result; + + return this.disabledUnlessAssociatedInputsChange + ? result && this._areReferencedInputsDirty + : result; } get data(): object | undefined { @@ -5566,7 +5608,7 @@ export class ShowCardAction extends Action { releaseDOMResources() { super.releaseDOMResources(); - + this.card.releaseDOMResources(); } @@ -5696,7 +5738,7 @@ class ActionCollection { } private refreshContainer() { - this._actionCardContainer.innerHTML = ""; + clearElement(this._actionCardContainer); if (!this._actionCard) { this._actionCardContainer.style.marginTop = "0px"; @@ -5785,7 +5827,10 @@ class ActionCollection { for (const renderedAction of this._renderedActions) { // Remove actions after selected action from tabOrder if the actions are oriented horizontally, to skip focus directly to expanded card - if (this._owner.hostConfig.actions.actionsOrientation == Enums.Orientation.Horizontal && afterSelectedAction) { + if ( + this._owner.hostConfig.actions.actionsOrientation == Enums.Orientation.Horizontal && + afterSelectedAction + ) { renderedAction.isFocusable = false; } @@ -6490,9 +6535,11 @@ export abstract class StylableCardElementContainer extends CardElementContainer if (ignoreBackgroundImages) { currentElementHasBackgroundImage = false; - } - else { - currentElementHasBackgroundImage = currentElement instanceof Container ? currentElement.backgroundImage.isValid() : false; + } else { + currentElementHasBackgroundImage = + currentElement instanceof Container + ? currentElement.backgroundImage.isValid() + : false; } if (currentElement instanceof StylableCardElementContainer) { @@ -6914,10 +6961,10 @@ export class Container extends ContainerBase { const registration = context.elementRegistry.findByName(typeName); if (registration?.singletonBehavior !== ElementSingletonBehavior.NotAllowed) { const element = context.parseElement( - this, - jsonItems, - [], - !this.isDesignMode(), + this, + jsonItems, + [], + !this.isDesignMode(), true ); @@ -6947,7 +6994,10 @@ export class Container extends ContainerBase { const collectionPropertyName = this.getItemsCollectionPropertyName(); - if ((this._items.length === 1) && (this._items[0].getElementSingletonBehavior() === ElementSingletonBehavior.Only)) { + if ( + this._items.length === 1 && + this._items[0].getElementSingletonBehavior() === ElementSingletonBehavior.Only + ) { // If the element is only allowed in a singleton context, parse it to an object instead of an array context.serializeValue(target, collectionPropertyName, this._items[0].toJSON(context)); } else { @@ -6960,7 +7010,10 @@ export class Container extends ContainerBase { } getEffectivePadding(): PaddingDefinition { - if (GlobalSettings.removePaddingFromContainersWithBackgroundImage && !this.getHasBackground(true)) { + if ( + GlobalSettings.removePaddingFromContainersWithBackgroundImage && + !this.getHasBackground(true) + ) { return new PaddingDefinition(); } @@ -7908,7 +7961,11 @@ export abstract class ContainerWithActions extends Container { getForbiddenActionNames(): string[] { // If the container can host singletons, and the only child element is a carousel, we should restrict the actions. - if (this.canHostSingletons() && this.getItemCount() === 1 && this.getItemAt(0).getJsonTypeName() === "Carousel") { + if ( + this.canHostSingletons() && + this.getItemCount() === 1 && + this.getItemAt(0).getJsonTypeName() === "Carousel" + ) { return ["Action.ToggleVisibility", "Action.ShowCard"]; } return []; @@ -8522,14 +8579,16 @@ export class SerializationContext extends BaseSerializationContext { if (source && typeof source === "object") { const oldForbiddenTypes = new Set(); - this._forbiddenTypes.forEach((type) => {oldForbiddenTypes.add(type)}); + this._forbiddenTypes.forEach((type) => { + oldForbiddenTypes.add(type); + }); forbiddenTypes.forEach((type) => { this._forbiddenTypes.add(type); }); const typeName = Utils.parseString(source["type"]); - const ignoreForbiddenType = parsingSingletonObject && (typeName === "Carousel"); + const ignoreForbiddenType = parsingSingletonObject && typeName === "Carousel"; if (typeName && this._forbiddenTypes.has(typeName) && !ignoreForbiddenType) { logParseEvent(typeName, Enums.TypeErrorType.ForbiddenType); diff --git a/source/nodejs/adaptivecards/src/utils.ts b/source/nodejs/adaptivecards/src/utils.ts index 9cf15094b4..f12918373f 100644 --- a/source/nodejs/adaptivecards/src/utils.ts +++ b/source/nodejs/adaptivecards/src/utils.ts @@ -115,7 +115,13 @@ export function stringToCssColor(color: string | undefined): string | undefined return color; } -export function truncate(element: HTMLElement, maxHeight: number, lineHeight?: number) { +function truncateWorker( + element: HTMLElement, + maxHeight: number, + fullText: string, + truncateAt: (text: string, idx: number) => void, + lineHeight?: number +) { const fits = () => { // Allow a one pixel overflow to account for rounding differences // between browsers @@ -126,11 +132,6 @@ export function truncate(element: HTMLElement, maxHeight: number, lineHeight?: n return; } - const fullText = element.innerHTML; - const truncateAt = (idx: any) => { - element.innerHTML = fullText.substring(0, idx) + "..."; - }; - const breakableIndices = findBreakableIndices(fullText); let lo = 0; let hi = breakableIndices.length; @@ -139,7 +140,7 @@ export function truncate(element: HTMLElement, maxHeight: number, lineHeight?: n // Do a binary search for the longest string that fits while (lo < hi) { const mid = Math.floor((lo + hi) / 2); - truncateAt(breakableIndices[mid]); + truncateAt(fullText, breakableIndices[mid]); if (fits()) { bestBreakIdx = breakableIndices[mid]; @@ -149,7 +150,7 @@ export function truncate(element: HTMLElement, maxHeight: number, lineHeight?: n } } - truncateAt(bestBreakIdx); + truncateAt(fullText, bestBreakIdx); // If we have extra room, try to expand the string letter by letter // (covers the case where we have to break in the middle of a long word) @@ -157,7 +158,7 @@ export function truncate(element: HTMLElement, maxHeight: number, lineHeight?: n let idx = findNextCharacter(fullText, bestBreakIdx); while (idx < fullText.length) { - truncateAt(idx); + truncateAt(fullText, idx); if (fits()) { bestBreakIdx = idx; @@ -167,10 +168,48 @@ export function truncate(element: HTMLElement, maxHeight: number, lineHeight?: n } } - truncateAt(bestBreakIdx); + truncateAt(fullText, bestBreakIdx); } } +export function truncateText(element: HTMLElement, maxHeight: number, lineHeight?: number) { + truncateWorker( + element, + maxHeight, + element.innerText, + (text: string, idx: number) => { + element.innerText = text.substring(0, idx) + "..."; + }, + lineHeight + ); +} + +/** + * {@link truncate} has been deprecated and is no longer in use internally. This policy passes + * content through as it always has, which is _supposed_ to be dealing with text only (see {@link + * TextBlock.truncateIfSupported}), but had a bug where it might actually pass through an element + * for which innerHTML yielded actual HTML (since fixed). + */ +const ttDeprecatedPolicy = window.trustedTypes?.createPolicy("deprecatedExportedFunctionPolicy", { + createHTML: (value) => value +}); + +/** @deprecated Use {@link truncateText} instead. */ +export function truncate(element: HTMLElement, maxHeight: number, lineHeight?: number) { + truncateWorker( + element, + maxHeight, + element.innerHTML, + (text: string, idx: number) => { + const truncatedString = text.substring(0, idx) + "..."; + const truncatedHTML = + ttDeprecatedPolicy?.createHTML(truncatedString) ?? truncatedString; + element.innerHTML = truncatedHTML as string; + }, + lineHeight + ); +} + function findBreakableIndices(html: string): number[] { const results: number[] = []; let idx = findNextCharacter(html, -1); @@ -225,4 +264,4 @@ export function clearElementChildren(element: HTMLElement) { while (element.firstChild) { element.removeChild(element.firstChild); } -} +} \ No newline at end of file diff --git a/source/nodejs/marked-schema/package-lock.json b/source/nodejs/marked-schema/package-lock.json index 543a9e8b49..8b4a9b67ed 100644 --- a/source/nodejs/marked-schema/package-lock.json +++ b/source/nodejs/marked-schema/package-lock.json @@ -11,8 +11,7 @@ "dependencies": { "json-schema-ref-parser": "^3.3.1", "minimist": "^1.2.5" - }, - "devDependencies": {} + } }, "node_modules/argparse": { "version": "1.0.10", diff --git a/source/nodejs/package-lock.json b/source/nodejs/package-lock.json index 826979fd3e..8e462d4c4f 100644 --- a/source/nodejs/package-lock.json +++ b/source/nodejs/package-lock.json @@ -11,6 +11,7 @@ "@types/jest": "^27.0.2", "@types/react": "^16.14.16", "@types/react-dom": "^16.9.14", + "@types/trusted-types": "^2.0.2", "@typescript-eslint/eslint-plugin": "^5.2.0", "@typescript-eslint/parser": "^5.2.0", "clang-format": "^1.5.0", @@ -3034,6 +3035,12 @@ "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", "dev": true }, + "node_modules/@types/trusted-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz", + "integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==", + "dev": true + }, "node_modules/@types/ws": { "version": "8.5.3", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", @@ -17527,6 +17534,12 @@ "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", "dev": true }, + "@types/trusted-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz", + "integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==", + "dev": true + }, "@types/ws": { "version": "8.5.3", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", diff --git a/source/nodejs/package.json b/source/nodejs/package.json index dc8b8a97ec..9fe04409ef 100644 --- a/source/nodejs/package.json +++ b/source/nodejs/package.json @@ -15,6 +15,7 @@ "@types/jest": "^27.0.2", "@types/react": "^16.14.16", "@types/react-dom": "^16.9.14", + "@types/trusted-types": "^2.0.2", "@typescript-eslint/eslint-plugin": "^5.2.0", "@typescript-eslint/parser": "^5.2.0", "clang-format": "^1.5.0", @@ -41,14 +42,14 @@ "rimraf": "^3.0.2", "sass": "^1.43.4", "style-loader": "^3.3.1", + "svg-url-loader": "^7.1.1", "ts-jest": "^27.0.7", "ts-loader": "^9.2.6", "typescript": "^4.4.4", "webpack": "^5.60.0", "webpack-cli": "^4.9.1", "webpack-concat-files-plugin": "^0.5.2", - "webpack-dev-server": "^4.3.1", - "svg-url-loader": "^7.1.1" + "webpack-dev-server": "^4.3.1" }, "clang-format-launcher": { "includeEndsWith": [