diff --git a/src/utils/rest.ts b/src/utils/rest.ts index ff17866..e927e9f 100644 --- a/src/utils/rest.ts +++ b/src/utils/rest.ts @@ -69,7 +69,63 @@ function fillHeaders(xhr: XMLHttpRequest, headers: { [key: string]: string; }) { } } -function getXhr(url: string, body?: IRequestBody, options?: IRestOptions, async = true): IRequestObjects { +/** + * Depending on async: + * - Returns an XMLHttpRequest that was opened asynchronously (needs to have event listeners set up before .send()) or + * - Returns an XMLHttpRequest that was opened synchronously (will block on .send()) + */ +function getXhr(url: string, body?: IRequestBody, options?: IRestOptions, async?: true): Promise +function getXhr(url: string, body?: IRequestBody, options?: IRestOptions, async?: false): IRequestObjects +function getXhr(url: string, body?: IRequestBody, options?: IRestOptions, async: boolean = false): IRequestObjects | Promise { + + let [myOptions, myCacheOptions] = configureXhrHeaders(url, body, options); + + const xhr: XMLHttpRequest = new XMLHttpRequest(); + xhr.open(myOptions.method, url, async); + + fillHeaders(xhr, myOptions.headers); + + if (myOptions.cors) { + xhr.withCredentials = true; + } + + if (!isNullOrEmptyString(myOptions.responseType) && myOptions.responseType !== "text") { + if (myCacheOptions.allowCache === true && + (myOptions.responseType === "blob" || myOptions.responseType === "arraybuffer" || myOptions.responseType === "document")) { + logger.warn("When allowCache is true, Blob, ArrayBuffer and Document response types will only be stored in runtime memory and not committed to local storage."); + } + xhr.responseType = myOptions.responseType; + } + + const needsDigest = + (myOptions.method === "GET" && myOptions.includeDigestInGet) || + (myOptions.method === "POST" && myOptions.includeDigestInPost); + + const applyDigest = (digest: string | null) => { + if (digest) { + xhr.setRequestHeader("X-RequestDigest", digest); + } else { + console.warn("X-RequestDigest header not set due to getFormDigest returning null"); + } + } + + const result: IRequestObjects = { xhr, options: myOptions, cacheOptions: myCacheOptions }; + + if (needsDigest && async) { + return getFormDigest(myOptions.spWebUrl, true).then(digest => { + applyDigest(digest); + return result; + }) + } else if (needsDigest && !async) { + const digest = getFormDigest(myOptions.spWebUrl, false); + applyDigest(digest); + return result; + } + + return result; +} + +function configureXhrHeaders(url: string, body?: IRequestBody, options?: IRestOptions): [IRestRequestOptions, IRestCacheOptions & { cacheKey?: string; }] { var optionsWithDefaults = assign({}, getDefaultOptions(), options); let myCacheOptions: IRestCacheOptions & { cacheKey?: string; } = {}; Object.keys(AllRestCacheOptionsKeys).forEach(key => { @@ -80,39 +136,19 @@ function getXhr(url: string, body?: IRequestBody, options?: IRestOptions, async }); let myOptions: IRestRequestOptions = { ...optionsWithDefaults }; - var xhr: XMLHttpRequest = new XMLHttpRequest(); - let jsonType = myOptions.jsonMetadata || jsonTypes.verbose; - if (myOptions.cors) { - xhr.withCredentials = true; - } - if (isNullOrUndefined(myOptions.headers)) myOptions.headers = {};//issue 660 in case the sender sent headers as null if (isNullOrUndefined(myOptions.headers["Accept"])) { myOptions.headers["Accept"] = jsonType; } - let method = myOptions.method; - if (isNullOrEmptyString(method)) { - method = isNullOrUndefined(body) ? "GET" : "POST"; + if (isNullOrEmptyString(myOptions.method)) { + myOptions.method = isNullOrUndefined(body) ? "GET" : "POST"; } - myOptions.method = method; - xhr.open(method, url, async !== false); - if (method === "GET") { - if (myOptions.includeDigestInGet === true) {//by default don't add it, unless explicitly asked in options - xhr.setRequestHeader("X-RequestDigest", getFormDigest(myOptions.spWebUrl)); - } - } - else if (method === "POST") { - if (isNullOrUndefined(myOptions.headers["content-type"])) { - myOptions.headers["content-type"] = jsonType; - } - - if (myOptions.includeDigestInPost !== false) {//if explicitly set to false - don't include it - xhr.setRequestHeader("X-RequestDigest", getFormDigest(myOptions.spWebUrl)); - } + if (myOptions.method === "POST" && isNullOrUndefined(myOptions.headers["content-type"])) { + myOptions.headers["content-type"] = jsonType; } if (!isNullOrEmptyString(myOptions.xHttpMethod)) { @@ -123,27 +159,13 @@ function getXhr(url: string, body?: IRequestBody, options?: IRestOptions, async } } - fillHeaders(xhr, myOptions.headers); - - if (!isNullOrEmptyString(myOptions.responseType) && myOptions.responseType !== "text") { - if (myCacheOptions.allowCache === true && - (myOptions.responseType === "blob" || myOptions.responseType === "arraybuffer" || myOptions.responseType === "document")) { - logger.warn("When allowCache is true, Blob, ArrayBuffer and Document response types will only be stored in runtime memory and not committed to local storage."); - } - xhr.responseType = myOptions.responseType; - } - //we do not support cache if there is a request body //postCacheKey - allow cache on post request for stuff like get item by CamlQuery if (isNullOrUndefined(body) || !isNullOrEmptyString(myOptions.postCacheKey)) { myCacheOptions.cacheKey = (url + '|' + JSON.stringify(myOptions)).trim().toLowerCase(); } - return { - xhr: xhr, - options: myOptions, - cacheOptions: myCacheOptions - }; + return [myOptions, myCacheOptions]; } function getCachedResult(objects: IRequestObjects): IJsonSyncResult { @@ -373,10 +395,9 @@ export function GetJsonSync(url: string, body?: IRequestBody, options?: IRest return syncResult; } -export function GetJson(url: string, body?: IRequestBody, options?: IRestOptions): Promise { +export async function GetJson(url: string, body?: IRequestBody, options?: IRestOptions): Promise { try { - let objects = getXhr(url, body, options); - + let objects = await getXhr(url, body, options, true); var cachedResult = getCachedResult(objects); if (!isNullOrUndefined(cachedResult)) { if (!supressDebugMessages) { @@ -489,7 +510,7 @@ export function GetJson(url: string, body?: IRequestBody, options?: IRestOpti return xhrPromise; } catch (e) { - return Promise.reject({ message: "an error occured" }); + return Promise.reject({ error: e, message: e.message, stack: e.stack }); } } @@ -499,4 +520,4 @@ export function GetJsonClearCache() { Object.keys(_cachedResults).forEach(key => { delete _cachedResults[key]; }); -} \ No newline at end of file +} diff --git a/src/utils/sharepoint.rest/web.ts b/src/utils/sharepoint.rest/web.ts index 1543d8d..0aec12c 100644 --- a/src/utils/sharepoint.rest/web.ts +++ b/src/utils/sharepoint.rest/web.ts @@ -1057,6 +1057,36 @@ export function GetContextWebInformationSync(siteUrl: string): IContextWebInform } } +export async function GetContextWebInformation(siteUrl: string): Promise { + var siteId: string = null; + if (hasGlobalContext() && _spPageContextInfo && _spPageContextInfo.isAppWeb) { + //inside an app web you can't get the contextinfo for any other site + siteUrl = _spPageContextInfo.webServerRelativeUrl; + siteId = _spPageContextInfo.siteId; + } else { + siteId = await GetSiteId(siteUrl); + + if (isNullOrEmptyString(siteId)) { + return null; + } + } + + try { + let result = await GetJson<{ + d: { GetContextWebInformation: IContextWebInformation; }; + }>(`${GetRestBaseUrl(siteUrl)}/contextinfo`, null, { + method: "POST", + maxAge: 5 * 60, + includeDigestInPost: false, + allowCache: true, + postCacheKey: `GetContextWebInformation_${normalizeGuid(siteId)}` + }); + return result.d.GetContextWebInformation; + } catch { + return null; + } +} + function _getCustomActionsBaseRestUrl(siteUrl?: string, options: { listId?: string, actionId?: string } = {}) { const { listId, actionId } = { ...options }; @@ -1274,9 +1304,17 @@ export async function GetWebPropertyByName(name: string, siteUrl?: string) { return null; } -export function getFormDigest(serverRelativeWebUrl?: string) { - var contextWebInformation = GetContextWebInformationSync(serverRelativeWebUrl); - return contextWebInformation && contextWebInformation.FormDigestValue || null; +export function getFormDigest(serverRelativeWebUrl?: string, async?: true): Promise +export function getFormDigest(serverRelativeWebUrl?: string, async?: false): string | null +export function getFormDigest(serverRelativeWebUrl?: string, async: boolean = false): string | null | Promise { + if (async) { + return GetContextWebInformation(serverRelativeWebUrl).then(contextWebInformation => { + return contextWebInformation && contextWebInformation.FormDigestValue || null; + }); + } else { + let contextWebInformation = GetContextWebInformationSync(serverRelativeWebUrl); + return contextWebInformation && contextWebInformation.FormDigestValue || null; + } } export interface spfxContext { legacyPageContext: typeof _spPageContextInfo }