import { ITokenService , IProfileProperties } from "@pnp/modern-search-extensibility" ; import { ServiceKey , ServiceScope , Log , UrlQueryParameterCollection } from "@microsoft/sp-core-library" ; import { PageContext } from '@microsoft/sp-page-context' ; import { SPHttpClient } from '@microsoft/sp-http' ; import { DateHelper } from "../../helpers/DateHelper" ; import { Constants } from "../../common/Constants" ; import { ObjectHelper } from "../../helpers/ObjectHelper" ; import { StringHelper } from "../../helpers/StringHelper" ; const TokenService_ServiceKey = 'ModernSearchTokenService' ; export enum BuiltinTokenNames { /**
     * The input query text configured in the search results Web Part
     */ inputQueryText = 'inputQueryText' , /**
     * Similar as 'inputQueryText' to match the SharePoint search token
     */ searchTerms = 'searchTerms' , /**
     * The current selected filters if any
     */ filters = 'filters' , /**
     * The current selected verticals if any
     */ verticals = 'verticals' } export class TokenService implements ITokenService { /**
     * This regex only matches expressions enclosed with single, not escaped, curly braces '{}'
     */ private genericTokenRegexp : RegExp = /{[^\{]+?[^\\]}/gi ; /**
     * The list of user properties. Used to avoid refetching it every time.
     */ private userProperties : IProfileProperties = null ; /**
     * The current page item. Used to avoid refetching it every time.
     */ private currentPageItem : any = null ; /**
     * The current service scope
     */ private serviceScope : ServiceScope ; /**
     * The current page context
     */ private pageContext : PageContext ; /**
     * The SPHttpClient instance
     */ private spHttpClient : SPHttpClient ; /**
     * The list of static tokens values set by the Web Part as context
     */ private tokenValuesList : { [ key ] : any ; } = { [ BuiltinTokenNames . inputQueryText ] : null , [ BuiltinTokenNames . searchTerms ] : null , [ BuiltinTokenNames . filters ] : null , [ BuiltinTokenNames . verticals ] : undefined } ; /**
     * A date helper instance
     */ private dateHelper : DateHelper ; /**
     * The moment.js library reference
     */ private moment : any ; public static ServiceKey : ServiceKey < ITokenService > = ServiceKey . create ( TokenService_ServiceKey , TokenService ) ; public constructor ( serviceScope ) { this . serviceScope = serviceScope ; serviceScope . whenFinished ( ( ) => { this . dateHelper = serviceScope . consume < DateHelper > ( DateHelper . ServiceKey ) ; this . pageContext = serviceScope . consume < PageContext > ( PageContext . serviceKey ) ; this . spHttpClient = serviceScope . consume < SPHttpClient > ( SPHttpClient . serviceKey ) ; } ) ; } public setTokenValue ( token , value ) { if ( Object . keys ( this . tokenValuesList ) . indexOf ( token ) !== - 1 ) { this . tokenValuesList [ token ] = value ; } else { Log . warn ( TokenService_ServiceKey , `The token '${ token }' not allowed.` ) ; } } public getTokenValue ( token ) { return this . tokenValuesList [ token ] ; } public async resolveTokens ( inputString ) { if ( inputString ) { this . moment = await this . dateHelper . moment ( ) ; inputString = await this . replacePageTokens ( inputString ) ; inputString = await this . replaceUserTokens ( inputString ) ; inputString = this . replaceDateTokens ( inputString ) ; inputString = this . replaceQueryStringTokens ( inputString ) ; inputString = this . replaceWebTokens ( inputString ) ; inputString = this . replacePageContextTokens ( inputString ) ; inputString = this . replaceSiteTokens ( inputString ) ; inputString = this . replaceListTokens ( inputString ) ; inputString = this . replaceGroupTokens ( inputString ) ; inputString = this . replaceLegacyPageContextTokens ( inputString ) ; inputString = await this . replaceHubTokens ( inputString ) ; inputString = inputString . replace ( /\{TenantUrl\}/gi , `https://` + window . location . host ) ; const tokens = inputString . match ( this . genericTokenRegexp ) ; if ( tokens !== null && tokens . length > 0 ) { tokens . forEach ( token => { const tokenName = token . substr ( 1 ) . slice ( 0 , - 1 ) ; const tokenValue = ObjectHelper . byPath ( this . tokenValuesList , tokenName ) ; if ( tokenValue !== undefined ) { if ( tokenValue !== null ) { inputString = inputString . replace ( new RegExp ( StringHelper . escapeRegExp ( token ) , 'gi' ) , tokenValue ) ; } else { inputString = inputString . replace ( new RegExp ( StringHelper . escapeRegExp ( token ) , 'gi' ) , '' ) ; } } } ) ; } inputString = this . replaceAndOrOperator ( inputString ) ; inputString = inputString . replace ( /\\(.)/gi , '$1' ) ; } return inputString ; } /**
     * Retrieves available current page item properties
     */ public async getPageProperties ( ) { let item = null ; if ( this . pageContext . listItem ) { const url = this . pageContext . web . absoluteUrl + `/_api/web/GetList(@v1)/RenderExtendedListFormData(itemId=${ this . pageContext . listItem . id },formId='viewform',mode='2',options=7)?@v1='${ this . pageContext . list . serverRelativeUrl }'` ; try { const response = await this . spHttpClient . post ( url , SPHttpClient . configurations . v1 , { headers : { 'X-ClientService-ClientTag' : Constants . X_CLIENTSERVICE_CLIENTTAG , 'UserAgent' : Constants . X_CLIENTSERVICE_CLIENTTAG } } ) ; if ( response . ok ) { let result = await response . json ( ) ; let itemRow = JSON . parse ( result . value ) ; item = Object . keys ( itemRow . Data . Row [ 0 ] ) . reduce ( ( c , k ) => ( c [ k ] = itemRow . Data . Row [ 0 ] [ k ] , c ) , { } ) ; } else { throw response . statusText ; } } catch ( error ) { const errorMessage = error ? error . message : `Failed to resolve page tokens` ; Log . error ( TokenService_ServiceKey , new Error ( `Error: '${ error }'` ) , this . serviceScope ) ; throw new Error ( errorMessage ) ; } } return item ; } /**
     * Retrieve all current user profile properties
     */ public async getUserProfileProperties ( ) { let responseJson = null ; let userProperties = { } ; const endpoint = `${ this . pageContext . web . absoluteUrl }/_api/SP.UserProfiles.PeopleManager/GetMyProperties` ; const response = await this . spHttpClient . get ( endpoint , SPHttpClient . configurations . v1 , { headers : { 'X-ClientService-ClientTag' : Constants . X_CLIENTSERVICE_CLIENTTAG , 'UserAgent' : Constants . X_CLIENTSERVICE_CLIENTTAG } } ) ; if ( response . ok ) { responseJson = await response . json ( ) ; if ( responseJson . UserProfileProperties ) { responseJson . UserProfileProperties . forEach ( property => { userProperties [ property . Key . toLowerCase ( ) ] = property . Value ; } ) ; } return userProperties ; } else { const errorMessage = `${ TokenService_ServiceKey }: Error retrieving user profiel properties. Details: ${ ( response as any ) . statusMessage ? ( response as any ) . statusMessage : response . status }` ; const error = new Error ( errorMessage ) ; Log . error ( TokenService_ServiceKey , error , this . serviceScope ) ; throw error ; } } param inputString private async replacePageTokens ( inputString ) { const pageTokenRegExp = /\{(?:Page)\.(.*?)\}/gi ; let matches = pageTokenRegExp . exec ( inputString ) ; let item = { } ; if ( matches != null && this . pageContext . listItem ) { let pageItem = this . currentPageItem ; if ( ! pageItem ) { pageItem = await this . getPageProperties ( ) ; } let properties = Object . keys ( pageItem ) ; properties . forEach ( property => { item [ property ] = pageItem [ property ] ; } ) ; item = this . recursivelyLowercaseJSONKeys ( item ) ; while ( matches !== null && item != null ) { const pageProperty = matches [ 1 ] ; let itemProp = '' ; if ( /\.Label|\.TermID/gi . test ( pageProperty ) ) { let term = pageProperty . split ( "." ) ; let columnName = term [ 0 ] . toLowerCase ( ) ; let labelOrTermId = term [ 1 ] . toLowerCase ( ) ; if ( Array . isArray ( item [ columnName ] ) && item [ columnName ] . length > 0 ) { itemProp = item [ columnName ] . map ( taxonomyValue => { return taxonomyValue [ labelOrTermId ] ; } ) . join ( ',' ) ; } else if ( ! Array . isArray ( item [ columnName ] ) && item [ columnName ] !== undefined && item [ columnName ] !== "" ) { itemProp = item [ columnName ] [ labelOrTermId ] ; } } else { itemProp = ObjectHelper . byPath ( item , pageProperty . toLowerCase ( ) ) ; } inputString = inputString . replace ( matches [ 0 ] , itemProp ) ; matches = pageTokenRegExp . exec ( inputString ) ; } } return inputString ; } param inputString private replacePageContextTokens ( inputString ) { const siteRegExp = /\{(?:PageContext)\.(.*?)\}/gi ; let matches = siteRegExp . exec ( inputString ) ; if ( matches != null ) { while ( matches !== null ) { const prop = matches [ 1 ] ; inputString = inputString . replace ( new RegExp ( matches [ 0 ] , "gi" ) , this . pageContext ? ObjectHelper . byPath ( this . pageContext , prop ) : '' ) ; matches = siteRegExp . exec ( inputString ) ; } } return inputString ; } param inputString private async replaceUserTokens ( inputString ) { const userTokenRegExp = /\{(?:User)\.(.*?)\}/gi ; let matches = userTokenRegExp . exec ( inputString . toLowerCase ( ) ) ; while ( matches !== null ) { let userProperty = matches [ 1 ] . toLowerCase ( ) ; if ( ! /^(audiences)$/gi . test ( userProperty ) ) { let propertyValue = null ; if ( ! /^(name|email)$/gi . test ( userProperty ) ) { if ( ! this . userProperties ) { this . userProperties = await this . getUserProfileProperties ( ) ; } propertyValue = this . userProperties [ userProperty ] ? this . userProperties [ userProperty ] : '' ; } else { switch ( userProperty ) { case "email" : propertyValue = this . pageContext . user . email ; break ; case "name" : propertyValue = this . pageContext . user . displayName ; break ; default : propertyValue = this . pageContext . user . displayName ; break ; } } const tokenExprToReplace = new RegExp ( matches [ 0 ] , 'gi' ) ; inputString = inputString . replace ( tokenExprToReplace , propertyValue ) ; } matches = userTokenRegExp . exec ( inputString ) ; } inputString = inputString . replace ( new RegExp ( "\{Me\}" , 'gi' ) , this . pageContext . user . displayName ) ; return inputString ; } param inputString private replaceDateTokens ( inputString ) { const currentDate = /\{CurrentDate\}/gi ; const currentMonth = /\{CurrentMonth\}/gi ; const currentYear = /\{CurrentYear\}/gi ; let results = /\{Today\s*[\+-]\s*\[{0,1}\d{1,}\]{0,1}\}/gi ; let match ; while ( ( match = results . exec ( inputString ) ) !== null ) { for ( let result of match ) { const operator = result . indexOf ( '+' ) !== - 1 ? '+' : '-' ; const addOrRemove = operator == '+' ? 1 : - 1 ; const operatorSplit = result . split ( operator ) ; const digit = parseInt ( operatorSplit [ operatorSplit . length - 1 ] . replace ( "{" , "" ) . replace ( "}" , "" ) . trim ( ) ) * addOrRemove ; let dt = new Date ( ) ; dt . setDate ( dt . getDate ( ) + digit ) ; const formatDate = this . moment ( dt ) . utc ( ) . format ( "YYYY-MM-DDTHH:mm:ss\\Z" ) ; inputString = inputString . replace ( result , formatDate ) ; } } let formattedDate = this . moment ( new Date ( ) ) . utc ( ) . format ( "YYYY-MM-DDTHH:mm:ss\\Z" ) ; inputString = inputString . replace ( new RegExp ( "{Today}" , 'gi' ) , formattedDate ) ; const d = new Date ( ) ; inputString = inputString . replace ( currentDate , d . getDate ( ) . toString ( ) ) ; inputString = inputString . replace ( currentMonth , ( d . getMonth ( ) + 1 ) . toString ( ) ) ; inputString = inputString . replace ( currentYear , d . getFullYear ( ) . toString ( ) ) ; return inputString ; } param inputString private replaceQueryStringTokens ( inputString ) { const webRegExp = /\{(?:\?){0,1}(?:QueryString)\.(.*?)\}/gi ; let modifiedString = inputString ; let matches = webRegExp . exec ( inputString ) ; if ( matches != null ) { const url = new URL ( window . location . href ) ; const queryParameters = new URLSearchParams ( url . search ) ; while ( matches !== null ) { const qsProp = matches [ 1 ] ; const itemProp = decodeURIComponent ( queryParameters . get ( qsProp ) || "" ) ; if ( itemProp ) { modifiedString = modifiedString . replace ( matches [ 0 ] , itemProp ) ; } else if ( matches [ 0 ] . includes ( "?" ) ) { modifiedString = modifiedString . replace ( matches [ 0 ] , "" ) ; } matches = webRegExp . exec ( inputString ) ; } } return modifiedString ; } param inputString private replaceWebTokens ( inputString ) { const queryStringVariables = /\{(?:Web)\.(.*?)\}/gi ; let matches = queryStringVariables . exec ( inputString ) ; if ( matches != null ) { while ( matches !== null ) { const webProp = matches [ 1 ] ; inputString = inputString . replace ( new RegExp ( matches [ 0 ] , "gi" ) , this . pageContext . web ? this . pageContext . web [ webProp ] : '' ) ; matches = queryStringVariables . exec ( inputString ) ; } } return inputString ; } param inputString private replaceSiteTokens ( inputString ) { const siteRegExp = /\{(?:Site)\.(.*?)\}/gi ; let matches = siteRegExp . exec ( inputString ) ; if ( matches != null ) { while ( matches !== null ) { const siteProp = matches [ 1 ] ; inputString = inputString . replace ( new RegExp ( matches [ 0 ] , "gi" ) , this . pageContext . site ? ObjectHelper . byPath ( this . pageContext . site , siteProp ) : '' ) ; matches = siteRegExp . exec ( inputString ) ; } } return inputString ; } param inputString private async replaceHubTokens ( inputString ) { const hubRegExp = /\{(?:Hub)\.(.*?)\}/gi ; let matches = hubRegExp . exec ( inputString ) ; const hubInfos = await this . getHubInfo ( ) ; if ( matches != null && hubInfos ) { while ( matches !== null ) { const hubProp = matches [ 1 ] ; inputString = inputString . replace ( new RegExp ( matches [ 0 ] , "gi" ) , hubInfos [ hubProp ] ) ; matches = hubRegExp . exec ( inputString ) ; } } return inputString ; } param inputString private replaceGroupTokens ( inputString ) { const groupRegExp = /\{(?:Group)\.(.*?)\}/gi ; let matches = groupRegExp . exec ( inputString ) ; if ( matches != null ) { while ( matches !== null ) { const groupProp = matches [ 1 ] ; inputString = inputString . replace ( new RegExp ( matches [ 0 ] , "gi" ) , this . pageContext . site . group ? ObjectHelper . byPath ( this . pageContext . site . group , groupProp ) : '' ) ; matches = groupRegExp . exec ( inputString ) ; } } return inputString ; } param inputString private replaceListTokens ( inputString ) { const listRegExp = /\{(?:List)\.(.*?)\}/gi ; let matches = listRegExp . exec ( inputString ) ; if ( matches != null ) { while ( matches !== null ) { const listProp = matches [ 1 ] ; inputString = inputString . replace ( new RegExp ( matches [ 0 ] , "gi" ) , this . pageContext . list ? ObjectHelper . byPath ( this . pageContext . list , listProp ) : '' ) ; matches = listRegExp . exec ( inputString ) ; } } return inputString ; } param inputString private replaceLegacyPageContextTokens ( inputString ) { const legacyPageContextRegExp = /\{(?:LegacyPageContext)\.(.*?)\}/gi ; let matches = legacyPageContextRegExp . exec ( inputString ) ; if ( matches != null ) { while ( matches !== null ) { const legacyProp = matches [ 1 ] ; inputString = inputString . replace ( new RegExp ( matches [ 0 ] , "gi" ) , this . pageContext . legacyPageContext ? ObjectHelper . byPath ( this . pageContext . legacyPageContext , legacyProp ) : '' ) ; matches = legacyPageContextRegExp . exec ( inputString ) ; } } return inputString ; } private replaceAndOrOperator ( inputString ) { const orAndConditionTokens = /\{(?:(\||\&)(.+?)(>=|=|<=|:|<>|<|>))(\{?.*?\}?\s*)\}/gi ; let reQueryTemplate = inputString ; let match = orAndConditionTokens . exec ( inputString ) ; if ( match != null ) { while ( match !== null ) { let conditions = [ ] ; const conditionOperator = match [ 1 ] ; const property = match [ 2 ] ; const operator = match [ 3 ] ; const tokenValue = match [ 4 ] ; let quotes = '"' ; let orAndOperator = conditionOperator === '|' ? 'OR' : 'AND' ; if ( ! /\{(?:User)\.(.*?)\}/gi . test ( tokenValue ) ) { const allValues = tokenValue . split ( ',' ) ; if ( allValues . length > 0 ) { allValues . forEach ( value => { conditions . push ( `(${ property }${ operator }${ /\s/g . test ( value ) ? `${ quotes }${ value }${ quotes }` : value })` ) ; } ) ; } else { conditions . push ( `(${ property }${ operator }${ /\s/g . test ( tokenValue ) ? `${ quotes }${ tokenValue }${ quotes }` : tokenValue })` ) ; } let condition = `${ conditions . join ( ` ${ orAndOperator } ` ) }` ; inputString = inputString . replace ( match [ 0 ] , condition ) ; } match = orAndConditionTokens . exec ( reQueryTemplate ) ; } } return inputString ; } /**
     * Get hub site data
     */ public async getHubInfo ( ) { try { const restUrl = `${ this . pageContext . site . absoluteUrl }/_api/site?$select=IsHubSite,HubSiteId,Id` ; const data = await this . spHttpClient . get ( restUrl , SPHttpClient . configurations . v1 , { headers : { 'X-ClientService-ClientTag' : Constants . X_CLIENTSERVICE_CLIENTTAG , 'UserAgent' : Constants . X_CLIENTSERVICE_CLIENTTAG } } ) ; if ( data && data . ok ) { const jsonData = await data . json ( ) ; if ( jsonData ) { return jsonData ; } } return null ; } catch ( error ) { Log . error ( TokenService_ServiceKey , new Error ( `Error while fetching Hub site data. Details: ${ error }` ) , this . serviceScope ) ; return null ; } } param obj private recursivelyLowercaseJSONKeys ( obj ) { const copyOfObj = obj ; if ( typeof copyOfObj !== 'object' || copyOfObj === null ) { return copyOfObj ; } if ( Array . isArray ( copyOfObj ) ) { return copyOfObj . map ( o => this . recursivelyLowercaseJSONKeys ( o ) ) ; } return Object . keys ( copyOfObj ) . reduce ( ( prev , curr ) => { prev [ curr . toLowerCase ( ) ] = this . recursivelyLowercaseJSONKeys ( copyOfObj [ curr ] ) ; return prev ; } , { } ) ; } }