From 6730f598becc8e2474abdc9bb35922555b8314bc Mon Sep 17 00:00:00 2001 From: Crash Collison <3751389+tehcrashxor@users.noreply.github.com> Date: Thu, 25 Jul 2024 22:16:43 -0700 Subject: [PATCH 1/2] Update to PAC 1.33.5 (#998) --- gulpfile.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gulpfile.mjs b/gulpfile.mjs index b084bb41..e3ec3c23 100644 --- a/gulpfile.mjs +++ b/gulpfile.mjs @@ -350,8 +350,8 @@ async function snapshot() { } } -const feedName = 'CAP_ISVExp_Tools_Stable'; -const cliVersion = '1.32.8'; +const feedName = 'nuget.org'; +const cliVersion = '1.33.5'; const recompile = gulp.series( clean, From 1dcf5da45c43bbe3abecc075f51c3dc08e2e7baf Mon Sep 17 00:00:00 2001 From: tyaginidhi Date: Fri, 26 Jul 2024 12:55:39 +0530 Subject: [PATCH 2/2] Validate web site preview URL (#1003) * Validate website preview URL * formatting updates --- l10n/bundle.l10n.json | 1 + .../vscode-powerplatform.xlf | 3 + src/client/extension.ts | 7 +- .../web/client/webExtensionTelemetryEvents.ts | 4 ++ src/common/services/ArtemisService.ts | 72 +++++++++---------- src/common/services/AuthenticationProvider.ts | 56 ++++++++++++++- src/common/services/BAPService.ts | 20 +++--- src/common/services/Constants.ts | 16 ++++- src/common/services/Interfaces.ts | 23 +++++- src/common/services/PPAPIService.ts | 65 +++++++++++++++++ src/common/services/TelemetryConstants.ts | 5 ++ src/web/client/WebExtensionContext.ts | 28 ++++++++ src/web/client/extension.ts | 19 ++--- src/web/client/utilities/commonUtil.ts | 32 +++++++++ .../webViews/powerPagesNavigationProvider.ts | 20 +++++- 15 files changed, 299 insertions(+), 72 deletions(-) create mode 100644 src/common/services/PPAPIService.ts diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 6d0db411..a2e82af1 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -8,6 +8,7 @@ "Preview site": "Preview site", "Open in Power Pages studio": "Open in Power Pages studio", "Preview site URL is not available": "Preview site URL is not available", + "Preview site URL is not valid": "Preview site URL is not valid", "Opening preview site...": "Opening preview site...", "Power Pages studio URL is not available": "Power Pages studio URL is not available", "Microsoft wants your feeback": "Microsoft wants your feeback", diff --git a/loc/translations-export/vscode-powerplatform.xlf b/loc/translations-export/vscode-powerplatform.xlf index 2ed83cf4..7c87315d 100644 --- a/loc/translations-export/vscode-powerplatform.xlf +++ b/loc/translations-export/vscode-powerplatform.xlf @@ -315,6 +315,9 @@ The {3} represents Dataverse Environment's Organization ID (GUID) Preview site URL is not available + + Preview site URL is not valid + Profile Kind: {0} The {0} represents the profile type (Admin vs Dataverse) diff --git a/src/client/extension.ts b/src/client/extension.ts index 6cdbe8a5..3997797e 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -38,7 +38,6 @@ import { oneDSLoggerWrapper } from "../common/OneDSLoggerTelemetry/oneDSLoggerWr import { OrgChangeNotifier, orgChangeEvent } from "./OrgChangeNotifier"; import { ActiveOrgOutput } from "./pac/PacTypes"; import { desktopTelemetryEventNames } from "../common/OneDSLoggerTelemetry/client/desktopExtensionTelemetryEventNames"; -import { IArtemisAPIOrgResponse } from "../common/services/Interfaces"; import { ArtemisService } from "../common/services/ArtemisService"; import { workspaceContainsPortalConfigFolder } from "../common/utilities/PathFinderUtil"; import { getPortalsOrgURLs } from "../common/utilities/WorkspaceInfoFinderUtil"; @@ -190,9 +189,9 @@ export async function activate( _context.subscriptions.push( orgChangeEvent(async (orgDetails: ActiveOrgOutput) => { const orgID = orgDetails.OrgId; - const artemisResponse = await ArtemisService.fetchArtemisResponse(orgID, _telemetry); - if (artemisResponse !== null && artemisResponse.length > 0) { - const { geoName, geoLongName } = artemisResponse[0]?.response as unknown as IArtemisAPIOrgResponse; + const artemisResponse = await ArtemisService.getArtemisResponse(orgID, _telemetry, ""); + if (artemisResponse !== null && artemisResponse.response !== null) { + const { geoName, geoLongName } = artemisResponse.response; oneDSLoggerWrapper.instantiate(geoName, geoLongName); oneDSLoggerWrapper.getLogger().traceInfo(desktopTelemetryEventNames.DESKTOP_EXTENSION_INIT_CONTEXT, { ...orgDetails, orgGeo: geoName }); } diff --git a/src/common/OneDSLoggerTelemetry/web/client/webExtensionTelemetryEvents.ts b/src/common/OneDSLoggerTelemetry/web/client/webExtensionTelemetryEvents.ts index f463ad8a..41df8e5c 100644 --- a/src/common/OneDSLoggerTelemetry/web/client/webExtensionTelemetryEvents.ts +++ b/src/common/OneDSLoggerTelemetry/web/client/webExtensionTelemetryEvents.ts @@ -74,6 +74,8 @@ export enum webExtensionTelemetryEventNames { WEB_EXTENSION_MULTI_FILE_FEATURE_FLAG_ENABLED = "WebExtensionMultiFileFeatureFlagEnabled", WEB_EXTENSION_MULTI_FILE_FEATURE_FLAG_DISABLED = "WebExtensionMultiFileFeatureFlagDisabled", WEB_EXTENSION_MULTI_FILE_MANDATORY_PARAMETERS_MISSING = "WebExtensionMultiFileMandatoryParametersMissing", + WEB_EXTENSION_WEBSITE_PREVIEW_URL_VALIDATION_SITE_DETAILS_FETCH_FAILED = "WebExtensionWebsitePreviewUrlValidationSiteDetailsFetchFailed", + WEB_EXTENSION_WEBSITE_PREVIEW_URL_VALIDATION_INSUFFICIENT_PARAMETERS = "WebExtensionWebsitePreviewUrlValidationInsufficientParameters", WEB_EXTENSION_MULTI_FILE_INVALID_DATAVERSE_URL = "WebExtensionMultiFileInvalidDataverseUrl", WEB_EXTENSION_MULTI_FILE_INVALID_WEBSITE_PREVIEW_URL = "WebExtensionMultiFileInvalidWebsitePreviewUrl", WEB_EXTENSION_CO_PRESENCE_FEATURE_FLAG_DISABLED = "WebExtensionCoPresenceFeatureFlagDisabled", @@ -100,6 +102,8 @@ export enum webExtensionTelemetryEventNames { WEB_EXTENSION_POWER_PAGES_WEB_VIEW_REGISTER_FAILED = 'webExtensionPowerPagesWebViewRegisterFailed', WEB_EXTENSION_BACK_TO_STUDIO_TRIGGERED = 'webExtensionBackToStudioTriggered', WEB_EXTENSION_PREVIEW_SITE_TRIGGERED = 'webExtensionPreviewSiteTriggered', + WEB_EXTENSION_WEBSITE_PREVIEW_URL_INVALID = "WebExtensionWebsitePreviewUrlInvalid", + WEB_EXTENSION_WEBSITE_PREVIEW_URL_UNAVAILABLE = "WebExtensionWebsitePreviewUrlUnavailable", WEB_EXTENSION_IMAGE_EDIT_SUPPORTED_FILE_EXTENSION = 'webExtensionImageEditSupportedFileExtension', WEB_EXTENSION_SAVE_IMAGE_FILE_TRIGGERED = 'webExtensionSaveImageFileTriggered', WEB_EXTENSION_FETCH_GET_OR_CREATE_SHARED_WORK_SPACE_ERROR = 'webExtensionFetchGetOrCreateSharedWorkSpaceError', diff --git a/src/common/services/ArtemisService.ts b/src/common/services/ArtemisService.ts index 79ab09aa..9410e853 100644 --- a/src/common/services/ArtemisService.ts +++ b/src/common/services/ArtemisService.ts @@ -8,55 +8,52 @@ import { COPILOT_UNAVAILABLE } from "../copilot/constants"; import { ITelemetry } from "../OneDSLoggerTelemetry/telemetry/ITelemetry"; import { sendTelemetryEvent } from "../copilot/telemetry/copilotTelemetry"; import { CopilotArtemisFailureEvent, CopilotArtemisSuccessEvent } from "../copilot/telemetry/telemetryConstants"; -import { BAPServiceStamp as BAPAPIEndpointStamp } from "./Constants"; -import { IArtemisAPIOrgResponse, IArtemisServiceEndpointInformation, IIntelligenceAPIEndpointInformation } from "./Interfaces"; +import { ServiceEndpointCategory } from "./Constants"; +import { IArtemisAPIOrgResponse, IArtemisServiceEndpointInformation, IArtemisServiceResponse, IIntelligenceAPIEndpointInformation } from "./Interfaces"; import { isCopilotDisabledInGeo, isCopilotSupportedInGeo } from "../copilot/utils/copilotUtil"; import { BAPService } from "./BAPService"; export class ArtemisService { public static async getIntelligenceEndpoint(orgId: string, telemetry: ITelemetry, sessionID: string, environmentId: string): Promise { - const artemisResponses = await ArtemisService.fetchArtemisResponse(orgId, telemetry, sessionID); + const artemisResponse = await ArtemisService.getArtemisResponse(orgId, telemetry, sessionID); - if (artemisResponses === null || artemisResponses.length === 0) { + if (artemisResponse === null) { return { intelligenceEndpoint: null, geoName: null, crossGeoDataMovementEnabledPPACFlag: false }; } + const { geoName, environment, clusterNumber } = artemisResponse.response as unknown as IArtemisAPIOrgResponse; + sendTelemetryEvent(telemetry, { eventName: CopilotArtemisSuccessEvent, copilotSessionId: sessionID, geoName: String(geoName), orgId: orgId }); - const artemisResponse = artemisResponses[0]; - if (artemisResponse !== null) { - const { geoName, environment, clusterNumber } = artemisResponse.response as unknown as IArtemisAPIOrgResponse; - sendTelemetryEvent(telemetry, { eventName: CopilotArtemisSuccessEvent, copilotSessionId: sessionID, geoName: String(geoName), orgId: orgId }); - - const crossGeoDataMovementEnabledPPACFlag = await BAPService.getCrossGeoCopilotDataMovementEnabledFlag(artemisResponse.stamp, telemetry, environmentId); - - if (isCopilotDisabledInGeo().includes(geoName)) { - return { intelligenceEndpoint: COPILOT_UNAVAILABLE, geoName: geoName, crossGeoDataMovementEnabledPPACFlag: crossGeoDataMovementEnabledPPACFlag }; - } - else if (crossGeoDataMovementEnabledPPACFlag === true) { - // Do nothing - we can make this call cross geo - } - else if (!isCopilotSupportedInGeo().includes(geoName)) { - return { intelligenceEndpoint: COPILOT_UNAVAILABLE, geoName: geoName, crossGeoDataMovementEnabledPPACFlag: crossGeoDataMovementEnabledPPACFlag }; - } + const crossGeoDataMovementEnabledPPACFlag = await BAPService.getCrossGeoCopilotDataMovementEnabledFlag(artemisResponse.stamp, telemetry, environmentId); - const intelligenceEndpoint = `https://aibuildertextapiservice.${geoName}-${'il' + clusterNumber}.gateway.${environment}.island.powerapps.com/v1.0/${orgId}/appintelligence/chat` - - return { intelligenceEndpoint: intelligenceEndpoint, geoName: geoName, crossGeoDataMovementEnabledPPACFlag: crossGeoDataMovementEnabledPPACFlag }; + if (isCopilotDisabledInGeo().includes(geoName)) { + return { intelligenceEndpoint: COPILOT_UNAVAILABLE, geoName: geoName, crossGeoDataMovementEnabledPPACFlag: crossGeoDataMovementEnabledPPACFlag }; + } + else if (crossGeoDataMovementEnabledPPACFlag === true) { + // Do nothing - we can make this call cross geo + } + else if (!isCopilotSupportedInGeo().includes(geoName)) { + return { intelligenceEndpoint: COPILOT_UNAVAILABLE, geoName: geoName, crossGeoDataMovementEnabledPPACFlag: crossGeoDataMovementEnabledPPACFlag }; } - return { intelligenceEndpoint: null, geoName: null, crossGeoDataMovementEnabledPPACFlag: false }; + const intelligenceEndpoint = `https://aibuildertextapiservice.${geoName}-${'il' + clusterNumber}.gateway.${environment}.island.powerapps.com/v1.0/${orgId}/appintelligence/chat` + + return { intelligenceEndpoint: intelligenceEndpoint, geoName: geoName, crossGeoDataMovementEnabledPPACFlag: crossGeoDataMovementEnabledPPACFlag }; } // Function to fetch Artemis response - public static async fetchArtemisResponse(orgId: string, telemetry: ITelemetry, sessionID = '') { + public static async getArtemisResponse(orgId: string, telemetry: ITelemetry, sessionID: string): Promise { const endpointDetails = ArtemisService.convertGuidToUrls(orgId); + const artemisResponses = await ArtemisService.fetchIslandInfo(endpointDetails, telemetry, sessionID); - const artemisResponse = await ArtemisService.fetchIslandInfo(endpointDetails, telemetry, sessionID); + if (artemisResponses === null || artemisResponses.length === 0) { + return null; + } - return artemisResponse; + return artemisResponses[0]; } - static async fetchIslandInfo(endpointDetails: IArtemisServiceEndpointInformation[], telemetry: ITelemetry, sessionID: string) { + static async fetchIslandInfo(endpointDetails: IArtemisServiceEndpointInformation[], telemetry: ITelemetry, sessionID: string): Promise { const requestInit: RequestInit = { method: 'GET', @@ -70,7 +67,7 @@ export class ArtemisService { if (!response.ok) { throw new Error('Request failed'); } - return { stamp: endpointDetail.stamp, response: await response.json() }; + return { stamp: endpointDetail.stamp, response: await response.json() as IArtemisAPIOrgResponse }; } catch (error) { return null; } @@ -78,14 +75,13 @@ export class ArtemisService { const results = await Promise.all(promises); const successfulResponses = results.filter(result => result !== null && result.response !== null); - return successfulResponses; + return successfulResponses as IArtemisServiceResponse[]; } catch (error) { sendTelemetryEvent(telemetry, { eventName: CopilotArtemisFailureEvent, copilotSessionId: sessionID, error: error as Error }) return null; } } - /** * @param orgId * @returns urls @@ -109,13 +105,13 @@ export class ArtemisService { const dodUrl = `https://${domain}.${nonProdSegment}.organization.api.appsplatform.us/gateway/cluster?app-version=1`; return [ - { stamp: BAPAPIEndpointStamp.TEST, endpoint: tstUrl }, - { stamp: BAPAPIEndpointStamp.PREPROD, endpoint: preprodUrl }, - { stamp: BAPAPIEndpointStamp.PROD, endpoint: prodUrl }, - { stamp: BAPAPIEndpointStamp.GCC, endpoint: gccUrl }, - { stamp: BAPAPIEndpointStamp.HIGH, endpoint: highUrl }, - { stamp: BAPAPIEndpointStamp.MOONCAKE, endpoint: mooncakeUrl }, - { stamp: BAPAPIEndpointStamp.DOD, endpoint: dodUrl }, + { stamp: ServiceEndpointCategory.TEST, endpoint: tstUrl }, + { stamp: ServiceEndpointCategory.PREPROD, endpoint: preprodUrl }, + { stamp: ServiceEndpointCategory.PROD, endpoint: prodUrl }, + { stamp: ServiceEndpointCategory.GCC, endpoint: gccUrl }, + { stamp: ServiceEndpointCategory.HIGH, endpoint: highUrl }, + { stamp: ServiceEndpointCategory.MOONCAKE, endpoint: mooncakeUrl }, + { stamp: ServiceEndpointCategory.DOD, endpoint: dodUrl }, ]; } diff --git a/src/common/services/AuthenticationProvider.ts b/src/common/services/AuthenticationProvider.ts index c918ff3f..f8ad53d5 100644 --- a/src/common/services/AuthenticationProvider.ts +++ b/src/common/services/AuthenticationProvider.ts @@ -18,14 +18,15 @@ import { VSCODE_EXTENSION_GRAPH_CLIENT_AUTHENTICATION_COMPLETED, VSCODE_EXTENSION_BAP_SERVICE_AUTHENTICATION_COMPLETED, VSCODE_EXTENSION_BAP_SERVICE_AUTHENTICATION_FAILED, - VSCODE_EXTENSION_DECODE_JWT_TOKEN_FAILED + VSCODE_EXTENSION_DECODE_JWT_TOKEN_FAILED, + VSCODE_EXTENSION_PPAPI_WEBSITES_AUTHENTICATION_COMPLETED, + VSCODE_EXTENSION_PPAPI_WEBSITES_AUTHENTICATION_FAILED } from "./TelemetryConstants"; import { ERROR_CONSTANTS } from "../ErrorConstants"; -import { BAP_SERVICE_SCOPE_DEFAULT, INTELLIGENCE_SCOPE_DEFAULT, PROVIDER_ID, SCOPE_OPTION_CONTACTS_READ, SCOPE_OPTION_DEFAULT, SCOPE_OPTION_OFFLINE_ACCESS, SCOPE_OPTION_USERS_READ_BASIC_ALL } from "./Constants"; +import { BAP_SERVICE_SCOPE_DEFAULT, INTELLIGENCE_SCOPE_DEFAULT, PPAPI_WEBSITES_SERVICE_SCOPE_DEFAULT, PROVIDER_ID, SCOPE_OPTION_CONTACTS_READ, SCOPE_OPTION_DEFAULT, SCOPE_OPTION_OFFLINE_ACCESS, SCOPE_OPTION_USERS_READ_BASIC_ALL } from "./Constants"; import jwt_decode from 'jwt-decode'; import { showErrorDialog } from "../utilities/errorHandlerUtil"; - export function getCommonHeadersForDataverse( accessToken: string, useOctetStreamContentType?: boolean @@ -292,3 +293,52 @@ export function getOIDFromToken(token: string, telemetry: ITelemetry) { } return ""; } + +export async function powerPlatformAPIAuthentication( + telemetry: ITelemetry, + firstTimeAuth = false +): Promise { + let accessToken = ""; + try { + let session = await vscode.authentication.getSession( + PROVIDER_ID, + [PPAPI_WEBSITES_SERVICE_SCOPE_DEFAULT], + { silent: true } + ); + + if (!session) { + session = await vscode.authentication.getSession( + PROVIDER_ID, + [PPAPI_WEBSITES_SERVICE_SCOPE_DEFAULT], + { createIfNone: true } + ); + } + + accessToken = session?.accessToken ?? ""; + if (!accessToken) { + throw new Error(ERROR_CONSTANTS.NO_ACCESS_TOKEN); + } + + if (firstTimeAuth) { + sendTelemetryEvent(telemetry, { + eventName: VSCODE_EXTENSION_PPAPI_WEBSITES_AUTHENTICATION_COMPLETED, + userId: + session?.account.id.split("/").pop() ?? + session?.account.id ?? + "", + }); + } + } catch (error) { + showErrorDialog( + vscode.l10n.t( + "Authorization Failed. Please run again to authorize it" + ), + vscode.l10n.t("There was a permissions problem with the server") + ); + sendTelemetryEvent(telemetry, + { eventName: VSCODE_EXTENSION_PPAPI_WEBSITES_AUTHENTICATION_FAILED, errorMsg: (error as Error).message } + ) + } + + return accessToken; +} diff --git a/src/common/services/BAPService.ts b/src/common/services/BAPService.ts index b9c607e7..fb26251a 100644 --- a/src/common/services/BAPService.ts +++ b/src/common/services/BAPService.ts @@ -6,11 +6,11 @@ import { ITelemetry } from "../OneDSLoggerTelemetry/telemetry/ITelemetry"; import { bapServiceAuthentication, getCommonHeaders } from "./AuthenticationProvider"; import { VSCODE_EXTENSION_GET_BAP_ENDPOINT_UNSUPPORTED_REGION, VSCODE_EXTENSION_GET_CROSS_GEO_DATA_MOVEMENT_ENABLED_FLAG_COMPLETED, VSCODE_EXTENSION_GET_CROSS_GEO_DATA_MOVEMENT_ENABLED_FLAG_FAILED } from "./TelemetryConstants"; -import { BAPServiceStamp, BAP_API_VERSION, BAP_SERVICE_COPILOT_CROSS_GEO_FLAG_RELATIVE_URL, BAP_SERVICE_ENDPOINT } from "./Constants"; +import { ServiceEndpointCategory, BAP_API_VERSION, BAP_SERVICE_COPILOT_CROSS_GEO_FLAG_RELATIVE_URL, BAP_SERVICE_ENDPOINT } from "./Constants"; import { sendTelemetryEvent } from "../copilot/telemetry/copilotTelemetry"; export class BAPService { - public static async getCrossGeoCopilotDataMovementEnabledFlag(serviceEndpointStamp: BAPServiceStamp, telemetry: ITelemetry, environmentId: string): Promise { + public static async getCrossGeoCopilotDataMovementEnabledFlag(serviceEndpointStamp: ServiceEndpointCategory, telemetry: ITelemetry, environmentId: string): Promise { try { const accessToken = await bapServiceAuthentication(telemetry, true); @@ -33,25 +33,25 @@ export class BAPService { return false; } - static async getBAPEndpoint(serviceEndpointStamp: BAPServiceStamp, telemetry: ITelemetry, environmentId: string): Promise { + static async getBAPEndpoint(serviceEndpointStamp: ServiceEndpointCategory, telemetry: ITelemetry, environmentId: string): Promise { let bapEndpoint = ""; switch (serviceEndpointStamp) { - case BAPServiceStamp.TEST: + case ServiceEndpointCategory.TEST: bapEndpoint = "https://test.api.bap.microsoft.com"; break; - case BAPServiceStamp.PREPROD: + case ServiceEndpointCategory.PREPROD: bapEndpoint = "https://preprod.api.bap.microsoft.com"; break; - case BAPServiceStamp.PROD: + case ServiceEndpointCategory.PROD: bapEndpoint = "https://api.bap.microsoft.com"; break; // All below endpoints are not supported yet - case BAPServiceStamp.DOD: - case BAPServiceStamp.GCC: - case BAPServiceStamp.HIGH: - case BAPServiceStamp.MOONCAKE: + case ServiceEndpointCategory.DOD: + case ServiceEndpointCategory.GCC: + case ServiceEndpointCategory.HIGH: + case ServiceEndpointCategory.MOONCAKE: default: sendTelemetryEvent(telemetry, { eventName: VSCODE_EXTENSION_GET_BAP_ENDPOINT_UNSUPPORTED_REGION, data: serviceEndpointStamp }); break; diff --git a/src/common/services/Constants.ts b/src/common/services/Constants.ts index 2af66651..c8b82cb8 100644 --- a/src/common/services/Constants.ts +++ b/src/common/services/Constants.ts @@ -12,12 +12,19 @@ export const SCOPE_OPTION_CONTACTS_READ = "Contacts.Read"; export const SCOPE_OPTION_USERS_READ_BASIC_ALL = "User.ReadBasic.All"; export const SCOPE_OPTION_DEFAULT = "/.default"; +// BAP API constants export const BAP_API_VERSION = '2021-04-01'; -export const BAP_SERVICE_SCOPE_DEFAULT = "https://api.bap.microsoft.com/.default";//"https://management.core.windows.net/.default"; +export const BAP_SERVICE_SCOPE_DEFAULT = "https://api.bap.microsoft.com/.default"; export const BAP_SERVICE_ENDPOINT = `{rootURL}/providers/Microsoft.BusinessAppPlatform/`; export const BAP_SERVICE_COPILOT_CROSS_GEO_FLAG_RELATIVE_URL = `scopes/admin/environments/{environmentID}?$expand=properties/copilotPolicies&api-version={apiVersion}`; -export enum BAPServiceStamp { +// PPAPI constants +export const PPAPI_WEBSITES_API_VERSION = '2022-03-01-preview'; +export const PPAPI_WEBSITES_SERVICE_SCOPE_DEFAULT = "https://api.powerplatform.com/.default"; +export const PPAPI_WEBSITES_ENDPOINT = `{rootURL}/powerpages/environments/{environmentId}/websites`; + +export enum ServiceEndpointCategory { + NONE = "", TEST = "test", PREPROD = "preprod", PROD = "prod", @@ -26,3 +33,8 @@ export enum BAPServiceStamp { MOONCAKE = "mooncake", DOD = "dod", } + +export enum WebsiteApplicationType { + Production = "Production", + Trial = "Trial", +} diff --git a/src/common/services/Interfaces.ts b/src/common/services/Interfaces.ts index d3ac0b73..e32457a3 100644 --- a/src/common/services/Interfaces.ts +++ b/src/common/services/Interfaces.ts @@ -3,15 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. */ -import { BAPServiceStamp } from "./Constants"; +import { ServiceEndpointCategory, WebsiteApplicationType } from "./Constants"; export interface IArtemisServiceEndpointInformation { - stamp: BAPServiceStamp; + stamp: ServiceEndpointCategory; endpoint: string; } export interface IArtemisServiceResponse { - stamp: BAPServiceStamp; + stamp: ServiceEndpointCategory; response: IArtemisAPIOrgResponse; } @@ -25,8 +25,25 @@ export interface IArtemisAPIOrgResponse { clusterType: string, } +export interface IArtemisServiceResponse { + stamp: ServiceEndpointCategory; + response: IArtemisAPIOrgResponse; +} + export interface IIntelligenceAPIEndpointInformation { intelligenceEndpoint: string | null, geoName: string | null, crossGeoDataMovementEnabledPPACFlag: boolean } + +export interface IWebsiteDetails { + websiteUrl: string; + dataverseInstanceUrl: string; + dataverseOrganizationId: string; + environmentId: string; + id: string; + siteVisibility: string; + tenantId: string; + websiteRecordId: string; + type: WebsiteApplicationType; +} diff --git a/src/common/services/PPAPIService.ts b/src/common/services/PPAPIService.ts new file mode 100644 index 00000000..1b9f959a --- /dev/null +++ b/src/common/services/PPAPIService.ts @@ -0,0 +1,65 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import { ITelemetry } from "../OneDSLoggerTelemetry/telemetry/ITelemetry"; +import { getCommonHeaders, powerPlatformAPIAuthentication } from "./AuthenticationProvider"; +import { VSCODE_EXTENSION_GET_CROSS_GEO_DATA_MOVEMENT_ENABLED_FLAG_FAILED, VSCODE_EXTENSION_GET_PPAPI_WEBSITES_ENDPOINT_UNSUPPORTED_REGION, VSCODE_EXTENSION_PPAPI_GET_WEBSITE_BY_ID_COMPLETED } from "./TelemetryConstants"; +import { ServiceEndpointCategory, PPAPI_WEBSITES_ENDPOINT, PPAPI_WEBSITES_API_VERSION } from "./Constants"; +import { sendTelemetryEvent } from "../copilot/telemetry/copilotTelemetry"; +import { IWebsiteDetails } from "./Interfaces"; + +export class PPAPIService { + public static async getWebsiteDetailsById(serviceEndpointStamp: ServiceEndpointCategory, environmentId: string, websitePreviewId: string, telemetry: ITelemetry): Promise { // websitePreviewId aka portalId + + try { + const accessToken = await powerPlatformAPIAuthentication(telemetry, true); + const response = await fetch(await PPAPIService.getPPAPIServiceEndpoint(serviceEndpointStamp, telemetry, environmentId, websitePreviewId), { + method: 'GET', + headers: getCommonHeaders(accessToken) + }); + + if (response.ok) { + const websiteDetails = await response.json() as unknown as IWebsiteDetails; + sendTelemetryEvent(telemetry, { eventName: VSCODE_EXTENSION_PPAPI_GET_WEBSITE_BY_ID_COMPLETED, orgUrl: websiteDetails.dataverseInstanceUrl }); + return websiteDetails; + } + } + catch (error) { + sendTelemetryEvent(telemetry, { eventName: VSCODE_EXTENSION_GET_CROSS_GEO_DATA_MOVEMENT_ENABLED_FLAG_FAILED, errorMsg: (error as Error).message }); + } + + return null; + } + + static async getPPAPIServiceEndpoint(serviceEndpointStamp: ServiceEndpointCategory, telemetry: ITelemetry, environmentId: string, websitePreviewId?: string): Promise { + + let ppapiEndpoint = ""; + + switch (serviceEndpointStamp) { + case ServiceEndpointCategory.TEST: + ppapiEndpoint = "https://api.test.powerplatform.com"; + break; + case ServiceEndpointCategory.PREPROD: + ppapiEndpoint = "https://api.preprod.powerplatform.com"; + break; + case ServiceEndpointCategory.PROD: + ppapiEndpoint = "https://api.powerplatform.com"; + break; + // All below endpoints are not supported yet + case ServiceEndpointCategory.DOD: + case ServiceEndpointCategory.GCC: + case ServiceEndpointCategory.HIGH: + case ServiceEndpointCategory.MOONCAKE: + default: + sendTelemetryEvent(telemetry, { eventName: VSCODE_EXTENSION_GET_PPAPI_WEBSITES_ENDPOINT_UNSUPPORTED_REGION, data: serviceEndpointStamp }); + break; + } + + return PPAPI_WEBSITES_ENDPOINT.replace("{rootURL}", ppapiEndpoint) + .replace("{environmentId}", environmentId) + + (websitePreviewId ? `/${websitePreviewId}` : '') + + `?api-version=${PPAPI_WEBSITES_API_VERSION}`; + } +} diff --git a/src/common/services/TelemetryConstants.ts b/src/common/services/TelemetryConstants.ts index 3300bfd3..4d4d2640 100644 --- a/src/common/services/TelemetryConstants.ts +++ b/src/common/services/TelemetryConstants.ts @@ -15,7 +15,12 @@ export const VSCODE_EXTENSION_GRAPH_CLIENT_AUTHENTICATION_FAILED = "VSCodeExtens export const VSCODE_EXTENSION_GRAPH_CLIENT_AUTHENTICATION_COMPLETED = "VSCodeExtensionGraphClientAuthenticationCompleted"; export const VSCODE_EXTENSION_BAP_SERVICE_AUTHENTICATION_FAILED = "VSCodeExtensionBAPServiceAuthenticationFailed"; export const VSCODE_EXTENSION_BAP_SERVICE_AUTHENTICATION_COMPLETED = "VSCodeExtensionBAPServiceAuthenticationCompleted"; +export const VSCODE_EXTENSION_PPAPI_WEBSITES_AUTHENTICATION_FAILED = "VSCodeExtensionPPAPIWebsitesAuthenticationFailed"; +export const VSCODE_EXTENSION_PPAPI_WEBSITES_AUTHENTICATION_COMPLETED = "VSCodeExtensionPPAPIWebsitesAuthenticationCompleted"; export const VSCODE_EXTENSION_GET_CROSS_GEO_DATA_MOVEMENT_ENABLED_FLAG_FAILED = "VSCodeExtensionGetCrossGeoDataMovementEnabledFlagFailed"; export const VSCODE_EXTENSION_GET_CROSS_GEO_DATA_MOVEMENT_ENABLED_FLAG_COMPLETED = "VSCodeExtensionGetCrossGeoDataMovementEnabledFlagCompleted"; export const VSCODE_EXTENSION_GET_BAP_ENDPOINT_UNSUPPORTED_REGION = "VSCodeExtensionGetBAPEndpointUnsupportedRegion"; +export const VSCODE_EXTENSION_GET_PPAPI_WEBSITES_ENDPOINT_UNSUPPORTED_REGION = "VSCodeExtensionGetPPAPIWebsitesEndpointUnsupportedRegion"; export const VSCODE_EXTENSION_DECODE_JWT_TOKEN_FAILED = "VSCodeExtensionDecodeJWTTokenFailed"; +export const VSCODE_EXTENSION_PPAPI_GET_WEBSITE_BY_ID_COMPLETED = "VSCodeExtensionPPAPIGetWebsiteByIdCompleted"; +export const VSCODE_EXTENSION_PPAPI_GET_WEBSITE_BY_ID_FAILED = "VSCodeExtensionPPAPIGetWebsiteByIdFailed"; diff --git a/src/web/client/WebExtensionContext.ts b/src/web/client/WebExtensionContext.ts index 6be2cf60..a67b95ca 100644 --- a/src/web/client/WebExtensionContext.ts +++ b/src/web/client/WebExtensionContext.ts @@ -34,6 +34,7 @@ import { EntityForeignKeyDataMap } from "./context/entityForeignKeyDataMap"; import { QuickPickProvider } from "./webViews/QuickPickProvider"; import { UserCollaborationProvider } from "./webViews/userCollaborationProvider"; import { GraphClientService } from "./services/graphClientService"; +import { ServiceEndpointCategory } from "../../common/services/Constants"; export interface IWebExtensionContext { // From portalSchema properties @@ -69,6 +70,9 @@ export interface IWebExtensionContext { isContextSet: boolean; currentSchemaVersion: string; websiteLanguageCode: string; + geoName: string; + geoLongName: string; + serviceEndpointCategory: ServiceEndpointCategory; // Telemetry and survey telemetry: WebExtensionTelemetry; @@ -103,6 +107,9 @@ class WebExtensionContext implements IWebExtensionContext { private _isContextSet: boolean; private _currentSchemaVersion: string; private _websiteLanguageCode: string; + private _geoName: string; + private _geoLongName: string; + private _serviceEndpointCategory: ServiceEndpointCategory; private _telemetry: WebExtensionTelemetry; private _npsEligibility: boolean; private _userId: string; @@ -187,6 +194,24 @@ class WebExtensionContext implements IWebExtensionContext { public get websiteLanguageCode() { return this._websiteLanguageCode; } + public get geoName() { + return this._geoName + } + public set geoName(name: string) { + this._geoName = name; + } + public get geoLongName() { + return this._geoLongName; + } + public set geoLongName(name: string) { + this._geoLongName = name; + } + public get serviceEndpointCategory() { + return this._serviceEndpointCategory; + } + public set serviceEndpointCategory(name: ServiceEndpointCategory) { + this._serviceEndpointCategory = name; + } public get telemetry() { return this._telemetry; } @@ -254,6 +279,9 @@ class WebExtensionContext implements IWebExtensionContext { this._isContextSet = false; this._currentSchemaVersion = ""; this._websiteLanguageCode = ""; + this._geoName = ""; + this._geoLongName = ""; + this._serviceEndpointCategory = ServiceEndpointCategory.NONE; this._telemetry = new WebExtensionTelemetry(); this._npsEligibility = false; this._userId = ""; diff --git a/src/web/client/extension.ts b/src/web/client/extension.ts index a65c2a69..2ac13891 100644 --- a/src/web/client/extension.ts +++ b/src/web/client/extension.ts @@ -41,9 +41,9 @@ import { sendingMessageToWebWorkerForCoPresence } from "./utilities/collaboratio import { ECSFeaturesClient } from "../../common/ecs-features/ecsFeatureClient"; import { PowerPagesAppName, PowerPagesClientName } from "../../common/ecs-features/constants"; import { IPortalWebExtensionInitQueryParametersTelemetryData } from "../../common/OneDSLoggerTelemetry/web/client/webExtensionTelemetryInterface"; -import { IArtemisAPIOrgResponse } from "../../common/services/Interfaces"; import { ArtemisService } from "../../common/services/ArtemisService"; import { showErrorDialog } from "../../common/utilities/errorHandlerUtil"; +import { ServiceEndpointCategory } from "../../common/services/Constants"; export function activate(context: vscode.ExtensionContext): void { // setup telemetry @@ -116,9 +116,9 @@ export function activate(context: vscode.ExtensionContext): void { ); logOneDSLogger(queryParamsMap); const orgId = queryParamsMap.get(queryParameters.ORG_ID) as string; - const { geoName, geoLongName } = await fetchArtemisData(orgId); - WebExtensionContext.telemetry.sendInfoTelemetry(webExtensionTelemetryEventNames.WEB_EXTENSION_ORG_GEO, { orgId: orgId, orgGeo: geoName }); - oneDSLoggerWrapper.instantiate(geoName, geoLongName); + await fetchArtemisData(orgId); + WebExtensionContext.telemetry.sendInfoTelemetry(webExtensionTelemetryEventNames.WEB_EXTENSION_ORG_GEO, { orgId: orgId, orgGeo: WebExtensionContext.geoName }); + oneDSLoggerWrapper.instantiate(WebExtensionContext.geoLongName, WebExtensionContext.geoLongName); WebExtensionContext.telemetry.sendExtensionInitPathParametersTelemetry( appName, @@ -649,18 +649,19 @@ function isActiveDocument(fileFsPath: string): boolean { ); } -async function fetchArtemisData(orgId: string): Promise { - const artemisResponse = await ArtemisService.fetchArtemisResponse(orgId, WebExtensionContext.telemetry.getTelemetryReporter()); - if (artemisResponse === null || artemisResponse.length === 0) { +async function fetchArtemisData(orgId: string) { + const artemisResponse = await ArtemisService.getArtemisResponse(orgId, WebExtensionContext.telemetry.getTelemetryReporter(), ""); + if (artemisResponse === null) { WebExtensionContext.telemetry.sendErrorTelemetry( webExtensionTelemetryEventNames.WEB_EXTENSION_ARTEMIS_RESPONSE_FAILED, fetchArtemisData.name, ARTEMIS_RESPONSE_FAILED ); - return { geo: "", geoLongName: "" } as unknown as IArtemisAPIOrgResponse; } - return artemisResponse[0]?.response as unknown as IArtemisAPIOrgResponse; + WebExtensionContext.geoName = artemisResponse?.response?.geoName ?? ""; + WebExtensionContext.geoLongName = artemisResponse?.response?.geoLongName ?? ""; + WebExtensionContext.serviceEndpointCategory = artemisResponse?.stamp ?? ServiceEndpointCategory.NONE; } function logOneDSLogger(queryParamsMap: Map) { diff --git a/src/web/client/utilities/commonUtil.ts b/src/web/client/utilities/commonUtil.ts index 97d28c2a..7d890fb6 100644 --- a/src/web/client/utilities/commonUtil.ts +++ b/src/web/client/utilities/commonUtil.ts @@ -22,6 +22,8 @@ import WebExtensionContext from "../WebExtensionContext"; import { SETTINGS_EXPERIMENTAL_STORE_NAME } from "../../../common/constants"; import { doesFileExist, getFileAttributePath, getFileEntityName, updateEntityColumnContent, updateFileDirtyChanges } from "./fileAndEntityUtil"; import { isWebFileV2 } from "./schemaHelperUtil"; +import { ServiceEndpointCategory } from "../../../common/services/Constants"; +import { PPAPIService } from "../../../common/services/PPAPIService"; // decodes file content to UTF-8 export function convertContentToUint8Array(content: string, isBase64Encoded: boolean): Uint8Array { @@ -303,3 +305,33 @@ export function getRangeForMultilineMatch(text: string, pattern: string, index: const range = new vscode.Range(startLine, startIndex, endLine, endIndex); return range; } + +export async function validateWebsitePreviewURL(): Promise { + const envId = WebExtensionContext.urlParametersMap?.get(queryParameters.ENV_ID); + const serviceEndpointStamp = WebExtensionContext.serviceEndpointCategory; + const websitePreviewId = WebExtensionContext.urlParametersMap?.get(queryParameters.PORTAL_ID); + + if (serviceEndpointStamp === ServiceEndpointCategory.NONE || !envId || !websitePreviewId) { + WebExtensionContext.telemetry.sendErrorTelemetry( + webExtensionTelemetryEventNames.WEB_EXTENSION_WEBSITE_PREVIEW_URL_VALIDATION_INSUFFICIENT_PARAMETERS, + validateWebsitePreviewURL.name, + `serviceEndpointStamp:${serviceEndpointStamp}, envId:${envId}, websitePreviewId:${websitePreviewId}` + ); + return false; + } + + const siteDetails = await PPAPIService.getWebsiteDetailsById(serviceEndpointStamp, envId, websitePreviewId, WebExtensionContext.telemetry.getTelemetryReporter()); + + if (siteDetails == null) { + WebExtensionContext.telemetry.sendErrorTelemetry( + webExtensionTelemetryEventNames.WEB_EXTENSION_WEBSITE_PREVIEW_URL_VALIDATION_SITE_DETAILS_FETCH_FAILED, + validateWebsitePreviewURL.name, + ); + return false; + } + + return siteDetails.websiteUrl.length !== 0 && + WebExtensionContext.urlParametersMap.get(queryParameters.WEBSITE_PREVIEW_URL) !== undefined && + siteDetails.websiteUrl.toLocaleLowerCase().trim() === WebExtensionContext.urlParametersMap.get(queryParameters.WEBSITE_PREVIEW_URL)?.toLocaleLowerCase().trim(); +} + diff --git a/src/web/client/webViews/powerPagesNavigationProvider.ts b/src/web/client/webViews/powerPagesNavigationProvider.ts index 9e231f75..93e0b156 100644 --- a/src/web/client/webViews/powerPagesNavigationProvider.ts +++ b/src/web/client/webViews/powerPagesNavigationProvider.ts @@ -7,13 +7,14 @@ import * as vscode from 'vscode'; import * as path from 'path'; import WebExtensionContext from "../WebExtensionContext"; import { httpMethod, queryParameters } from '../common/constants'; -import { getBackToStudioURL, isStringUndefinedOrEmpty } from '../utilities/commonUtil'; +import { getBackToStudioURL, isStringUndefinedOrEmpty, validateWebsitePreviewURL } from '../utilities/commonUtil'; import { webExtensionTelemetryEventNames } from '../../../common/OneDSLoggerTelemetry/web/client/webExtensionTelemetryEvents'; export class PowerPagesNavigationProvider implements vscode.TreeDataProvider { private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; + private isWebsitePreviewURLValid: Promise = validateWebsitePreviewURL(); refresh(): void { this._onDidChangeTreeData.fire(); @@ -68,8 +69,21 @@ export class PowerPagesNavigationProvider implements vscode.TreeDataProvider