import { PromiseCache } from "@fluidframework/common-utils" ; import { IFluidCodeDetails , IRequest , isFluidPackage } from "@fluidframework/core-interfaces" ; import { IResolvedUrl , IUrlResolver } from "@fluidframework/driver-definitions" ; import { ITelemetryBaseLogger , ITelemetryLogger } from "@fluidframework/common-definitions" ; import { NonRetryableError } from "@fluidframework/driver-utils" ; import { PerformanceEvent } from "@fluidframework/telemetry-utils" ; import { IOdspResolvedUrl , IdentityType , isTokenFromCache , OdspResourceTokenFetchOptions , TokenFetcher , OdspErrorType , } from "@fluidframework/odsp-driver-definitions" ; import { getLocatorFromOdspUrl , storeLocatorInOdspUrl , encodeOdspFluidDataStoreLocator , locatorQueryParamName , } from "./odspFluidFileLink" ; import { OdspFluidDataStoreLocator , SharingLinkHeader } from "./contractsPublic" ; import { createOdspUrl } from "./createOdspUrl" ; import { OdspDriverUrlResolver } from "./odspDriverUrlResolver" ; import { getOdspResolvedUrl , createOdspLogger } from "./odspUtils" ; import { getFileLink } from "./getFileLink" ; /**
 * Properties passed to the code responsible for fetching share link for a file.
 */ export interface ShareLinkFetcherProps { /**
     * Callback method that is used to fetch access token necessary to call API that produces share link
     */ tokenFetcher : TokenFetcher < OdspResourceTokenFetchOptions > ; /**
     * Identity type determining the shape of share link as it differs for Enterprise and Consumer users.
     */ identityType : IdentityType ; } /**
 * Resolver to resolve urls like the ones created by createOdspUrl which is driver inner
 * url format and the ones which have things like driveId, siteId, itemId etc encoded in nav param.
 * This resolver also handles share links and try to generate one for the use by the app.
 */ export class OdspDriverUrlResolverForShareLink implements IUrlResolver { private readonly logger : ITelemetryLogger ; private readonly sharingLinkCache = new PromiseCache < string , string > ( ) ; private readonly shareLinkFetcherProps : ShareLinkFetcherProps | undefined ; param shareLinkFetcherProps param logger param appName public constructor ( shareLinkFetcherProps ? , logger ? , private readonly appName ? ) { this . logger = createOdspLogger ( logger ) ; if ( shareLinkFetcherProps ) { this . shareLinkFetcherProps = { ... shareLinkFetcherProps , tokenFetcher : this . toInstrumentedTokenFetcher ( this . logger , shareLinkFetcherProps . tokenFetcher ) } ; } } /**
     * Takes an already generated data store url (from requestUrl) and appends a path to the
     * existing data store information.
     */ public appendDataStorePath ( requestUrl , pathToAppend ) { const fluidInfo = getLocatorFromOdspUrl ( requestUrl ) ; if ( ! fluidInfo ) { return undefined ; } const parsingUrl = new URL ( fluidInfo . dataStorePath , `${ requestUrl . protocol }//${ requestUrl . hostname }` ) ; if ( pathToAppend . startsWith ( "/?" ) || pathToAppend . startsWith ( "?" ) ) { const queryParams = new URLSearchParams ( pathToAppend ) ; queryParams . forEach ( ( value , key ) => { parsingUrl . searchParams . append ( key , value ) ; } ) ; fluidInfo . dataStorePath = `${ parsingUrl . pathname }${ parsingUrl . search }` ; } else { fluidInfo . dataStorePath = `${ parsingUrl . pathname }${ parsingUrl . pathname . endsWith ( "/" ) || pathToAppend . startsWith ( "/" ) ? "" : "/" }${ pathToAppend }/${ parsingUrl . search }` ; } storeLocatorInOdspUrl ( requestUrl , fluidInfo ) ; return requestUrl . href ; } private getKey ( resolvedUrl ) { return `${ resolvedUrl . siteUrl },${ resolvedUrl . driveId },${ resolvedUrl . itemId }` ; } /**
     * Resolves request URL into driver details
     */ public async resolve ( request ) { const requestToBeResolved = { headers : request . headers , url : request . url } ; const isSharingLinkToRedeem = requestToBeResolved . headers ?. [ SharingLinkHeader . isSharingLinkToRedeem ] ; try { const url = new URL ( request . url ) ; const odspFluidInfo = getLocatorFromOdspUrl ( url ) ; if ( odspFluidInfo ) { requestToBeResolved . url = createOdspUrl ( odspFluidInfo ) ; } } catch { } const odspResolvedUrl = await new OdspDriverUrlResolver ( ) . resolve ( requestToBeResolved ) ; if ( isSharingLinkToRedeem ) { odspResolvedUrl . sharingLinkToRedeem = this . removeNavParam ( request . url ) ; odspResolvedUrl . shareLinkInfo = Object . assign ( odspResolvedUrl . shareLinkInfo || { } , { sharingLinkToRedeem : odspResolvedUrl . sharingLinkToRedeem } ) ; } if ( odspResolvedUrl . itemId ) { this . getShareLinkPromise ( odspResolvedUrl ) . catch ( ( ) => { } ) ; } return odspResolvedUrl ; } private removeNavParam ( link ) { const url = new URL ( link ) ; const params = new URLSearchParams ( url . search ) ; params . delete ( locatorQueryParamName ) ; url . search = params . toString ( ) ; return url . href ; } private toInstrumentedTokenFetcher ( logger , tokenFetcher ) { return async ( options ) => { return PerformanceEvent . timedExecAsync ( logger , { eventName : "GetSharingLinkToken" } , async ( event ) => tokenFetcher ( options ) . then ( ( tokenResponse ) => { if ( tokenResponse === null ) { throw new NonRetryableError ( "shareLinkTokenIsNull" , "Token callback returned null" , OdspErrorType . fetchTokenError ) ; } event . end ( { fromCache : isTokenFromCache ( tokenResponse ) } ) ; return tokenResponse ; } ) ) ; } ; } private async getShareLinkPromise ( resolvedUrl ) { if ( this . shareLinkFetcherProps === undefined ) { throw new Error ( "Failed to get share link because share link fetcher props are missing" ) ; } if ( ! ( resolvedUrl . siteUrl && resolvedUrl . driveId && resolvedUrl . itemId ) ) { throw new Error ( "Failed to get share link because necessary information is missing " + "(e.g. siteUrl, driveId or itemId)" ) ; } const key = this . getKey ( resolvedUrl ) ; const cachedLinkPromise = this . sharingLinkCache . get ( key ) ; if ( cachedLinkPromise ) { return cachedLinkPromise ; } const newLinkPromise = getFileLink ( this . shareLinkFetcherProps . tokenFetcher , resolvedUrl , this . shareLinkFetcherProps . identityType , this . logger ) . catch ( ( error ) => { this . logger . sendErrorEvent ( { eventName : "FluidFileUrlError" } , error ) ; this . sharingLinkCache . remove ( key ) ; throw error ; } ) ; this . sharingLinkCache . add ( key , async ( ) => newLinkPromise ) ; return newLinkPromise ; } param resolvedUrl param request public async getAbsoluteUrl ( resolvedUrl , dataStorePath , codeDetails ? ) { const odspResolvedUrl = getOdspResolvedUrl ( resolvedUrl ) ; const shareLink = await this . getShareLinkPromise ( odspResolvedUrl ) ; const shareLinkUrl = new URL ( shareLink ) ; const containerPackageName = isFluidPackage ( codeDetails ?. package ) ? codeDetails ?. package . name : codeDetails ?. package ?? odspResolvedUrl . codeHint ?. containerPackageName ; storeLocatorInOdspUrl ( shareLinkUrl , { siteUrl : odspResolvedUrl . siteUrl , driveId : odspResolvedUrl . driveId , itemId : odspResolvedUrl . itemId , dataStorePath , appName : this . appName , containerPackageName , fileVersion : odspResolvedUrl . fileVersion } ) ; return shareLinkUrl . href ; } /**
     * Crafts a supported document/driver URL
     */ public static createDocumentUrl ( baseUrl , driverInfo ) { const url = new URL ( baseUrl ) ; storeLocatorInOdspUrl ( url , driverInfo ) ; return url . href ; } /**
     * Crafts a supported data store nav param
     */ public static createNavParam ( locator ) { return encodeOdspFluidDataStoreLocator ( locator ) ; } }