Skip to content

Commit

Permalink
Merge branch 'main' into maggieg/add_card_cec
Browse files Browse the repository at this point in the history
  • Loading branch information
TrevorJoelHarris committed Sep 25, 2024
2 parents d6a0b87 + 60eb045 commit 1b31bca
Show file tree
Hide file tree
Showing 21 changed files with 421 additions and 238 deletions.
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"cSpell.words": ["adal", "frameless", "ipados", "teamspace", "uninitialize", "xvfb"],
"cSpell.words": ["adal", "frameless", "ipados", "teamsjs", "teamspace", "uninitialize", "xvfb"],
"editor.defaultFormatter": "esbenp.prettier-vscode",
"eslint.workingDirectories": [
"./apps/ssr-test-app/",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Added logging for version on startup",
"packageName": "@microsoft/teams-js",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Updated `pages.navigateToApp` to now optionally accept a more type-safe input object",
"packageName": "@microsoft/teams-js",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "Updated internal app id validation",
"packageName": "@microsoft/teams-js",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Added logging for current teamsjs instance and timestamps",
"packageName": "@microsoft/teams-js",
"email": "[email protected]",
"dependentChangeType": "patch"
}
9 changes: 3 additions & 6 deletions packages/teams-js/src/internal/appIdValidation.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { hasScriptTags } from './utils';

/**
* This function can be used to validate if a string is a "valid" app id.
* Valid is a relative term, in this case. Truly valid app ids are UUIDs as documented in the schema:
Expand All @@ -10,7 +12,7 @@
* @throws Error with a message describing the exact validation violation
*/
export function validateStringAsAppId(potentialAppId: string): void {
if (doesStringContainScriptTags(potentialAppId)) {
if (hasScriptTags(potentialAppId)) {
throw new Error(`Potential app id (${potentialAppId}) is invalid; it contains script tags.`);
}

Expand All @@ -25,11 +27,6 @@ export function validateStringAsAppId(potentialAppId: string): void {
}
}

export function doesStringContainScriptTags(str: string): boolean {
const scriptRegex = /<script[^>]*>[\s\S]*?<\/script[^>]*>/gi;
return scriptRegex.test(str);
}

export const minimumValidAppIdLength = 4;
export const maximumValidAppIdLength = 256;

Expand Down
12 changes: 12 additions & 0 deletions packages/teams-js/src/internal/telemetry.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
import { debug as registerLogger, Debugger } from 'debug';

import { UUID } from './uuidObject';

// Each teamsjs instance gets a unique identifier that will be prepended to every log statement
export const teamsJsInstanceIdentifier = new UUID();

// Every log statement will get prepended with the teamsJsInstanceIdentifier and a timestamp
const originalFormatArgsFunction = registerLogger.formatArgs;
registerLogger.formatArgs = function (args) {
args[0] = `(${new Date().toISOString()}): ${args[0]} [${teamsJsInstanceIdentifier.toString()}]`;
originalFormatArgsFunction.call(this, args);
};

const topLevelLogger = registerLogger('teamsJs');

/**
Expand Down
50 changes: 13 additions & 37 deletions packages/teams-js/src/internal/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,16 +272,16 @@ export function runWithTimeout<TResult, TError>(
* @internal
* Limited to Microsoft-internal use
*/
export function createTeamsAppLink(params: pages.NavigateToAppParams): string {
export function createTeamsAppLink(params: pages.AppNavigationParameters): string {
const url = new URL(
'https://teams.microsoft.com/l/entity/' +
encodeURIComponent(params.appId) +
encodeURIComponent(params.appId.toString()) +
'/' +
encodeURIComponent(params.pageId),
);

if (params.webUrl) {
url.searchParams.append('webUrl', params.webUrl);
url.searchParams.append('webUrl', params.webUrl.toString());
}
if (params.chatId || params.channelId || params.subPageId) {
url.searchParams.append(
Expand Down Expand Up @@ -460,43 +460,19 @@ export function fullyQualifyUrlString(fullOrRelativePath: string): URL {
}

/**
* The hasScriptTags function first decodes any HTML entities in the input string using the decodeHTMLEntities function.
* It then tries to decode the result as a URI component. If the URI decoding fails (which would throw an error), it assumes that the input was not encoded and uses the original input.
* Next, it defines a regular expression scriptRegex that matches any string that starts with <script (followed by any characters), then has any characters (including newlines),
* and ends with </script> (preceded by any characters).
* Finally, it uses the test method to check if the decoded input matches this regular expression. The function returns true if a match is found and false otherwise.
* @param input URL converted to string to pattern match
* Detects if there are any script tags in a given string, even if they are Uri encoded or encoded as HTML entities.
* @param input string to test for script tags
* @returns true if the input string contains a script tag, false otherwise
*/
function hasScriptTags(input: string): boolean {
let decodedInput;
try {
const decodedHTMLInput = decodeHTMLEntities(input);
decodedInput = decodeURIComponent(decodedHTMLInput);
} catch (e) {
// input was not encoded, use it as is
decodedInput = input;
}
const scriptRegex = /<script[^>]*>[\s\S]*?<\/script[^>]*>/gi;
return scriptRegex.test(decodedInput);
}
export function hasScriptTags(input: string): boolean {
const openingScriptTagRegex = /<script[^>]*>|&lt;script[^&]*&gt;|%3Cscript[^%]*%3E/gi;
const closingScriptTagRegex = /<\/script[^>]*>|&lt;\/script[^&]*&gt;|%3C\/script[^%]*%3E/gi;

/**
* The decodeHTMLEntities function replaces HTML entities in the input string with their corresponding characters.
*/
function decodeHTMLEntities(input: string): string {
const entityMap = new Map<string, string>([
['&lt;', '<'],
['&gt;', '>'],
['&amp;', '&'],
['&quot;', '"'],
['&#39;', "'"],
['&#x2F;', '/'],
]);
entityMap.forEach((value, key) => {
input = input.replace(new RegExp(key, 'gi'), value);
});
return input;
const openingOrClosingScriptTagRegex = new RegExp(
`${openingScriptTagRegex.source}|${closingScriptTagRegex.source}`,
'gi',
);
return openingOrClosingScriptTagRegex.test(input);
}

function isIdLengthValid(id: string): boolean {
Expand Down
14 changes: 8 additions & 6 deletions packages/teams-js/src/private/externalAppAuthentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { sendMessageToParentAsync } from '../internal/communication';
import { ensureInitialized } from '../internal/internalAPIs';
import { ApiName, ApiVersionNumber, getApiVersionTag } from '../internal/telemetry';
import { validateId, validateUrl } from '../internal/utils';
import { AppId } from '../public';
import { errorNotSupportedOnPlatform, FrameContexts } from '../public/constants';
import { runtime } from '../public/runtime';

Expand Down Expand Up @@ -351,7 +352,7 @@ export namespace externalAppAuthentication {
if (!isSupported()) {
throw errorNotSupportedOnPlatform;
}
validateId(appId, new Error('App id is not valid.'));
const typeSafeAppId: AppId = new AppId(appId);
validateOriginalRequestInfo(originalRequestInfo);

// Ask the parent window to open an authentication window with the parameters provided by the caller.
Expand All @@ -362,7 +363,7 @@ export namespace externalAppAuthentication {
),
'externalAppAuthentication.authenticateAndResendRequest',
[
appId,
typeSafeAppId.toString(),
originalRequestInfo,
authenticateParameters.url.href,
authenticateParameters.width,
Expand Down Expand Up @@ -395,14 +396,15 @@ export namespace externalAppAuthentication {
if (!isSupported()) {
throw errorNotSupportedOnPlatform;
}
validateId(appId, new Error('App id is not valid.'));
const typeSafeAppId: AppId = new AppId(appId);

return sendMessageToParentAsync(
getApiVersionTag(
externalAppAuthenticationTelemetryVersionNumber,
ApiName.ExternalAppAuthentication_AuthenticateWithSSO,
),
'externalAppAuthentication.authenticateWithSSO',
[appId, authTokenRequest.claims, authTokenRequest.silent],
[typeSafeAppId.toString(), authTokenRequest.claims, authTokenRequest.silent],
).then(([wasSuccessful, error]: [boolean, InvokeError]) => {
if (!wasSuccessful) {
throw error;
Expand Down Expand Up @@ -431,7 +433,7 @@ export namespace externalAppAuthentication {
if (!isSupported()) {
throw errorNotSupportedOnPlatform;
}
validateId(appId, new Error('App id is not valid.'));
const typeSafeAppId: AppId = new AppId(appId);

validateOriginalRequestInfo(originalRequestInfo);

Expand All @@ -441,7 +443,7 @@ export namespace externalAppAuthentication {
ApiName.ExternalAppAuthentication_AuthenticateWithSSOAndResendRequest,
),
'externalAppAuthentication.authenticateWithSSOAndResendRequest',
[appId, originalRequestInfo, authTokenRequest.claims, authTokenRequest.silent],
[typeSafeAppId.toString(), originalRequestInfo, authTokenRequest.claims, authTokenRequest.silent],
).then(([wasSuccessful, response]: [boolean, IInvokeResponse | InvokeErrorWrapper]) => {
if (wasSuccessful && response.responseType != null) {
return response as IInvokeResponse;
Expand Down
10 changes: 5 additions & 5 deletions packages/teams-js/src/private/externalAppCardActions.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { sendMessageToParentAsync } from '../internal/communication';
import { ensureInitialized } from '../internal/internalAPIs';
import { ApiName, ApiVersionNumber, getApiVersionTag } from '../internal/telemetry';
import { validateId } from '../internal/utils';
import { AppId } from '../public';
import { errorNotSupportedOnPlatform, FrameContexts } from '../public/constants';
import { runtime } from '../public/runtime';
import { ActionOpenUrlError, ActionSubmitError, IAdaptiveCardActionSubmit } from './interfaces';
Expand Down Expand Up @@ -48,15 +48,15 @@ export namespace externalAppCardActions {
if (!isSupported()) {
throw errorNotSupportedOnPlatform;
}
validateId(appId, new Error('App id is not valid.'));
const typeSafeAppId: AppId = new AppId(appId);

return sendMessageToParentAsync<[boolean, ActionSubmitError]>(
getApiVersionTag(
externalAppCardActionsTelemetryVersionNumber,
ApiName.ExternalAppCardActions_ProcessActionSubmit,
),
'externalAppCardActions.processActionSubmit',
[appId, actionSubmitPayload],
[typeSafeAppId.toString(), actionSubmitPayload],
).then(([wasSuccessful, error]: [boolean, ActionSubmitError]) => {
if (!wasSuccessful) {
throw error;
Expand Down Expand Up @@ -87,14 +87,14 @@ export namespace externalAppCardActions {
if (!isSupported()) {
throw errorNotSupportedOnPlatform;
}
validateId(appId, new Error('App id is not valid.'));
const typeSafeAppId: AppId = new AppId(appId);
return sendMessageToParentAsync<[ActionOpenUrlError, ActionOpenUrlType]>(
getApiVersionTag(
externalAppCardActionsTelemetryVersionNumber,
ApiName.ExternalAppCardActions_ProcessActionOpenUrl,
),
'externalAppCardActions.processActionOpenUrl',
[appId, url.href, fromElement],
[typeSafeAppId.toString(), url.href, fromElement],
).then(([error, response]: [ActionOpenUrlError, ActionOpenUrlType]) => {
if (error) {
throw error;
Expand Down
6 changes: 3 additions & 3 deletions packages/teams-js/src/private/externalAppCommands.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { sendMessageToParentAsync } from '../internal/communication';
import { ensureInitialized } from '../internal/internalAPIs';
import { ApiName, ApiVersionNumber, getApiVersionTag } from '../internal/telemetry';
import { validateId } from '../internal/utils';
import { AppId } from '../public';
import { errorNotSupportedOnPlatform, FrameContexts } from '../public/constants';
import { runtime } from '../public/runtime';
import { ExternalAppErrorCode } from './constants';
Expand Down Expand Up @@ -135,12 +135,12 @@ export namespace externalAppCommands {
if (!isSupported()) {
throw errorNotSupportedOnPlatform;
}
validateId(appId, new Error('App id is not valid.'));
const typeSafeAppId: AppId = new AppId(appId);

const [error, response] = await sendMessageToParentAsync<[ActionCommandError, IActionCommandResponse]>(
getApiVersionTag(externalAppCommandsTelemetryVersionNumber, ApiName.ExternalAppCommands_ProcessActionCommands),
ApiName.ExternalAppCommands_ProcessActionCommands,
[appId, commandId, extractedParameters],
[typeSafeAppId.toString(), commandId, extractedParameters],
);
if (error) {
throw error;
Expand Down
8 changes: 7 additions & 1 deletion packages/teams-js/src/public/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -756,7 +756,13 @@ export namespace app {
}
}

// This is called right away to make sure that we capture which script is being executed correctly
// This is called right away to make sure that we capture which script is being executed and important stats about the current teamsjs instance
appLogger(
'teamsjs instance is version %s, starting at %s UTC (%s local)',
version,
new Date().toISOString(),
new Date().toLocaleString(),
);
logWhereTeamsJsIsBeingUsed();

/**
Expand Down
3 changes: 2 additions & 1 deletion packages/teams-js/src/public/appId.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import { validateStringAsAppId } from '../internal/appIdValidation';
* However, there are some older internal/hard-coded apps which violate this schema and use names like
* com.microsoft.teamspace.tab.youtube. For compatibility with these legacy apps, we unfortunately cannot
* securely and completely validate app ids as UUIDs. Based on this, the validation is limited to checking
* for script tags, length, and non-printable characters.
* for script tags, length, and non-printable characters. Validation will be updated in the future to ensure
* the app id is a valid UUID as legacy apps update.
*/
export class AppId {
/**
Expand Down
Loading

0 comments on commit 1b31bca

Please sign in to comment.