UNPKG

@itwin/core-frontend

Version:
165 lines 9.28 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ /** @packageDocumentation * @module Tiles */ import { ArcGisErrorCode, ArcGisUtilities, MapLayerImageryProvider, MapLayerImageryProviderStatus } from "../../../../tile/internal"; import { IModelApp } from "../../../../IModelApp"; import { NotifyMessageDetails, OutputMessagePriority } from "../../../../NotificationManager"; import { headersIncludeAuthMethod } from "../../../../request/utils"; /** Base class for ArcGIS map-layer imagery providers. * * The initial purpose of this class is to offer shared methods * to query ArcGIS services and apply the appropriate security token. * @see [[ArcGISMapLayerImageryProvider]] * @internal */ export class ArcGISImageryProvider extends MapLayerImageryProvider { _accessClient; _lastAccessToken; /** Flag indicating if access token should be added to request. * @note We assume a service to require access token for the entire viewing session. */ _accessTokenRequired = false; _querySupported = false; get supportsMapFeatureInfo() { return this._querySupported; } constructor(settings, usesCachedTiles) { super(settings, usesCachedTiles); this._accessClient = IModelApp.mapLayerFormatRegistry?.getAccessClient(settings.formatId); } /** Updates the accessClient token state whenever the status of the provider change. * */ onStatusUpdated(status) { if (status === MapLayerImageryProviderStatus.RequireAuth) { // Invalidate the token, so a new one get generated if (this._accessClient?.invalidateToken !== undefined && this._lastAccessToken !== undefined) { this._accessClient.invalidateToken(this._lastAccessToken); } // Make sure we don't re-use this token again (i.e force login process) this._lastAccessToken = undefined; } } /** * Fetch an ArcGIS service metadata, and returns its JSON representation. * This wrapper maintains token state and should be used instead of the the ArcGisUtilities version. */ async getServiceJson() { let metadata; try { metadata = await ArcGisUtilities.getServiceJson({ url: this._settings.url, formatId: this._settings.formatId, userName: this._settings.userName, password: this._settings.password, queryParams: this._settings.collectQueryParams() }); } catch { } if (metadata && metadata.accessTokenRequired) { const accessClient = IModelApp.mapLayerFormatRegistry.getAccessClient(this._settings.formatId); if (accessClient) { try { // Keep track of last used access token, so we can invalidate it later when an errors occurs const accessToken = await accessClient.getAccessToken({ mapLayerUrl: new URL(this._settings.url) }); this._lastAccessToken = accessToken; } catch { } } // By turning this ON, tiles requests will include security token this._accessTokenRequired = metadata.accessTokenRequired; } return metadata; } /** * Make a request to an ArcGIS service using the provided URL and init parameters. * @param url URL to query * @param options Custom settings to apply to the request. * Refer to fetch API for more details (https://developer.mozilla.org/en-US/docs/Web/API/fetch) */ async fetch(url, options) { let errorCode; const urlObj = new URL(url); const queryParams = this._settings.collectQueryParams(); Object.keys(queryParams).forEach((paramKey) => { if (!urlObj.searchParams.has(paramKey)) urlObj.searchParams.append(paramKey, queryParams[paramKey]); }); if (this._accessTokenRequired && this._accessClient) { this._lastAccessToken = await ArcGisUtilities.appendSecurityToken(urlObj, this._accessClient, { mapLayerUrl: new URL(this._settings.url), userName: this._settings.userName, password: this._settings.password }); } // We want to complete the first request before letting other requests go; // this done to avoid flooding server with requests missing credentials if (!this._firstRequestPromise) this._firstRequestPromise = new Promise((resolve) => this.onFirstRequestCompleted.addOnce(() => resolve())); else await this._firstRequestPromise; let response; try { response = await fetch(urlObj, { ...options, credentials: this._includeUserCredentials ? "include" : undefined }); if (response.status === 401 && !this._lastAccessToken && headersIncludeAuthMethod(response.headers, ["ntlm", "negotiate"])) { // We got a http 401 challenge, lets try again with SSO enabled (i.e. Windows Authentication) response = await fetch(urlObj, { ...options, credentials: "include" }); if (response.status === 200) { this._includeUserCredentials = true; // avoid going through 401 challenges over and over } } if ((this._lastAccessToken && response.status === 400) || response.headers.get("content-type")?.toLowerCase().includes("htm")) { // For some reasons when we make a request with the fetch() api and there is a token error // we receive a status 400 instead of proper json response. (i.e doing the same request in the browser gives a different response) // For some other request, we also seen error message in html. // When it occurs, we fall back to root service request so we get a proper JSON response with error code. const tmpUrl = new URL(this._settings.url); if (this._lastAccessToken && this._accessTokenRequired) tmpUrl.searchParams.append("token", this._lastAccessToken.token); tmpUrl.searchParams.append("f", "json"); response = await fetch(tmpUrl.toString(), options); } errorCode = await ArcGisUtilities.checkForResponseErrorCode(response); if (errorCode !== undefined && (errorCode === ArcGisErrorCode.TokenRequired || errorCode === ArcGisErrorCode.InvalidToken || errorCode === ArcGisErrorCode.MissingPermissions)) { if (this._settings.userName && this._settings.userName.length > 0 && this._lastAccessToken) { // **** Legacy token ONLY *** // Token might have expired, make a second attempt by forcing new token. if (this._accessClient?.invalidateToken !== undefined && this._lastAccessToken !== undefined) this._accessClient.invalidateToken(this._lastAccessToken); const urlObj2 = new URL(url); if (this._accessClient) { try { this._lastAccessToken = await ArcGisUtilities.appendSecurityToken(urlObj, this._accessClient, { mapLayerUrl: urlObj, userName: this._settings.userName, password: this._settings.password }); } catch { } } // Make a second attempt with refreshed token response = await fetch(urlObj2.toString(), options); errorCode = await ArcGisUtilities.checkForResponseErrorCode(response); } if (errorCode !== undefined && (errorCode === ArcGisErrorCode.TokenRequired || errorCode === ArcGisErrorCode.InvalidToken || errorCode === ArcGisErrorCode.MissingPermissions)) { // Looks like the initially generated token has expired. if (this.status === MapLayerImageryProviderStatus.Valid) { // Only report new status change to avoid spamming the UI this.setStatus(MapLayerImageryProviderStatus.RequireAuth); this.onStatusChanged.raiseEvent(this); const msg = IModelApp.localization.getLocalizedString("iModelJs:MapLayers.Messages.FetchTooltipTokenError", { layerName: this._settings.name }); IModelApp.notifications.outputMessage(new NotifyMessageDetails(OutputMessagePriority.Warning, msg)); } } } } finally { this.onFirstRequestCompleted.raiseEvent(); } if (response === undefined) throw new Error("fetch call failed"); return response; } } //# sourceMappingURL=ArcGISImageryProvider.js.map