diff --git a/entry/src/main/ets/Interceptors/CommonHeaderInterceptor/viewmodel/CommonHeaderViewModel.ets b/entry/src/main/ets/Interceptors/CommonHeaderInterceptor/viewmodel/CommonHeaderViewModel.ets index 15d80bbe7d560e0f2b6e024368861bff9845cef5..57c83b03ff495908dcc84c3471155ea2a5fd11c5 100644 --- a/entry/src/main/ets/Interceptors/CommonHeaderInterceptor/viewmodel/CommonHeaderViewModel.ets +++ b/entry/src/main/ets/Interceptors/CommonHeaderInterceptor/viewmodel/CommonHeaderViewModel.ets @@ -43,9 +43,12 @@ export class CommonHeaderViewModel { */ onControllerAttached(controller: webview.WebviewController): void { try { + // [Start BindSchemeHandler] // Bind interceptor to HTTP controller.setWebSchemeHandler('http', this.schemeHandler); + // [End BindSchemeHandler] + // [Start Intercept] // Set up request interceptor this.schemeHandler.onRequestStart((request: webview.WebSchemeHandlerRequest, resourceHandler: webview.WebResourceHandler) => { @@ -53,6 +56,7 @@ export class CommonHeaderViewModel { const handled = this.model.processRequest(request, resourceHandler); return handled; }); + // [End Intercept] Logger.info(TAG, `HTTP protocol interceptor set up`); } catch (error) { diff --git a/entry/src/main/ets/Interceptors/CustomLoadingStrategyInterceptor/model/CustomLoadingStrategyModel.ets b/entry/src/main/ets/Interceptors/CustomLoadingStrategyInterceptor/model/CustomLoadingStrategyModel.ets index ddee2df1d4f0978b63384c55936fce1437530481..b29a114ae709ce5ae88a64ba5920893701d792b7 100644 --- a/entry/src/main/ets/Interceptors/CustomLoadingStrategyInterceptor/model/CustomLoadingStrategyModel.ets +++ b/entry/src/main/ets/Interceptors/CustomLoadingStrategyInterceptor/model/CustomLoadingStrategyModel.ets @@ -33,6 +33,50 @@ export class CustomLoadingStrategyModel { this.imageFormatList = formats; } + // [Start SetResponse] + // [Start IsImage] + // [Start GetRequestUrl] + // [Start IsWifi] + /** + * Processes the intercepted request and returns appropriate response + * Returns null if request should be allowed through + */ + processRequest(event: OnInterceptRequestEvent | null): WebResourceResponse | null { + // [StartExclude IsImage] + // [StartExclude GetRequestUrl] + // [StartExclude IsWifi] + // [StartExclude SetResponse] + if (!event || !event.request) { + return null; + } + // [EndExclude IsWifi] + // If WiFi network, allow original request + if (this.isWifiNetwork()) { + return null; + } + // [EndExclude GetRequestUrl] + // [StartExclude IsWifi] + const requestUrl = event.request.getRequestUrl(); + // [StartExclude GetRequestUrl] + // [EndExclude IsImage] + // Only intercept image requests + if (!this.isImageRequestUrl(requestUrl)) { + return null; // Not an image, allow original request + } + // [EndExclude SetResponse] + // [StartExclude IsImage] + // Replace with placeholder image + return this.createPlaceholderResponse(); + // [EndExclude IsImage] + // [EndExclude GetRequestUrl] + // [EndExclude IsWifi] + } + // [End IsWifi] + // [End GetRequestUrl] + // [End IsImage] + // [End SetResponse] + + // [Start IsWifi] /** * Checks if currently connected to a Wi-Fi network */ @@ -42,12 +86,16 @@ export class CustomLoadingStrategyModel { const netData = connection.getNetCapabilitiesSync(netHandle); return netData.bearerTypes.includes(connection.NetBearType.BEARER_WIFI); } catch (error) { + // [StartExclude IsWifi] let err = error as BusinessError; Logger.error(TAG, `Get the network info failed, errCode: ${err.code}, errMessage: ${err.message}.`); + // [EndExclude IsWifi] return false; } } + // [End IsWifi] + // [Start IsImage] /** * Checks if a URL request is for an image */ @@ -59,7 +107,9 @@ export class CustomLoadingStrategyModel { } return false; } + // [End IsImage] + // [Start SetResponse] /** * Creates a placeholder image response for non-WiFi networks */ @@ -77,28 +127,5 @@ export class CustomLoadingStrategyModel { response.setResponseIsReady(true); return response; } - - /** - * Processes the intercepted request and returns appropriate response - * Returns null if request should be allowed through - */ - processRequest(event: OnInterceptRequestEvent | null): WebResourceResponse | null { - if (!event || !event.request) { - return null; - } - - // If WiFi network, allow original request - if (this.isWifiNetwork()) { - return null; - } - - const requestUrl = event.request.getRequestUrl(); - // Only intercept image requests - if (!this.isImageRequestUrl(requestUrl)) { - return null; // Not an image, allow original request - } - - // Replace with placeholder image - return this.createPlaceholderResponse(); - } + // [End SetResponse] } diff --git a/entry/src/main/ets/Interceptors/LocalResourceInterceptor/model/LocalResourceModel.ets b/entry/src/main/ets/Interceptors/LocalResourceInterceptor/model/LocalResourceModel.ets index 38297f8a52e0706e1e6b352c62519ddc7e8154cf..b708e78053d17d66c7aff7ff3b9dac8d4d9298ae 100644 --- a/entry/src/main/ets/Interceptors/LocalResourceInterceptor/model/LocalResourceModel.ets +++ b/entry/src/main/ets/Interceptors/LocalResourceInterceptor/model/LocalResourceModel.ets @@ -25,6 +25,7 @@ export class LocalResourceModel { this.mimeTypeMap = mimeTypeMap; } + // [Start MapRemoteAndLocal] /** * Updates the scheme and mime type mappings */ @@ -32,26 +33,53 @@ export class LocalResourceModel { this.schemeMap = schemeMap; this.mimeTypeMap = mimeTypeMap; } + // [Start MapRemoteAndLocal] + // [Start SetResponse] + // [Start GetLocalResource] + // [Start GetRequestUrl] /** - * Gets the matching URL scheme key from the map - * Finds the longest matching key to prioritize specific paths over general ones + * Processes the intercepted request and returns local resource response if applicable + * Returns null if request should be allowed through */ - getUrlSchemeFromMap(prefix: string): string { - let matchedKey: string = ''; - let maxLength: number = 0; + processRequest(event: OnInterceptRequestEvent | null): WebResourceResponse | null { + // [StartExclude SetResponse] + // [StartExclude GetLocalResource] + // [StartExclude GetRequestUrl] + if (!event || !event.request) { + return null; + } + // [EndExclude GetRequestUrl] + const requestUrl = event.request.getRequestUrl(); + // [EndExclude GetLocalResource] + // [StartExclude GetRequestUrl] + const key = this.getUrlSchemeFromMap(requestUrl); - // Find the longest matching key to prioritize specific paths over general ones - for (let key of this.schemeMap.keys()) { - if (prefix.startsWith(key) && key.length > maxLength) { - matchedKey = key; - maxLength = key.length; - } + if (key.length === 0) { + return null; // No match, allow original request } - return matchedKey; + const rawfileName = this.schemeMap.get(key); + if (!rawfileName) { + return null; // Invalid mapping, allow original request + } + + const mimeType = this.mimeTypeMap.get(rawfileName); + if (!mimeType) { + return null; // Invalid mapping, allow original request + } + // [EndExclude SetResponse] + // [StartExclude GetLocalResource] + // Create response with local file + return this.createLocalResourceResponse(rawfileName, mimeType); + // [EndExclude GetLocalResource] + // [EndExclude GetRequestUrl] } + // [End GetRequestUrl] + // [End GetLocalResource] + // [End SetResponse] + // [Start SetResponse] /** * Creates a response with local file data */ @@ -69,34 +97,26 @@ export class LocalResourceModel { response.setResponseIsReady(true); return response; } + // [End SetResponse] + // [Start GetLocalResource] /** - * Processes the intercepted request and returns local resource response if applicable - * Returns null if request should be allowed through + * Gets the matching URL scheme key from the map + * Finds the longest matching key to prioritize specific paths over general ones */ - processRequest(event: OnInterceptRequestEvent | null): WebResourceResponse | null { - if (!event || !event.request) { - return null; - } - - const requestUrl = event.request.getRequestUrl(); - const key = this.getUrlSchemeFromMap(requestUrl); - - if (key.length === 0) { - return null; // No match, allow original request - } - - const rawfileName = this.schemeMap.get(key); - if (!rawfileName) { - return null; // Invalid mapping, allow original request - } + getUrlSchemeFromMap(prefix: string): string { + let matchedKey: string = ''; + let maxLength: number = 0; - const mimeType = this.mimeTypeMap.get(rawfileName); - if (!mimeType) { - return null; // Invalid mapping, allow original request + // Find the longest matching key to prioritize specific paths over general ones + for (let key of this.schemeMap.keys()) { + if (prefix.startsWith(key) && key.length > maxLength) { + matchedKey = key; + maxLength = key.length; + } } - // Create response with local file - return this.createLocalResourceResponse(rawfileName, mimeType); + return matchedKey; } + // [End GetLocalResource] } diff --git a/entry/src/main/ets/Interceptors/LocalResourceInterceptor/view/LocalResourceView.ets b/entry/src/main/ets/Interceptors/LocalResourceInterceptor/view/LocalResourceView.ets index 18666e830bed1b7521e0ca135b2788077f3552b2..4576b8f1f66612de9c88d67ade7799f6ce7e5d7a 100644 --- a/entry/src/main/ets/Interceptors/LocalResourceInterceptor/view/LocalResourceView.ets +++ b/entry/src/main/ets/Interceptors/LocalResourceInterceptor/view/LocalResourceView.ets @@ -33,6 +33,7 @@ struct LocalResource { @State isLoading: boolean = false; controller: webview.WebviewController = new webview.WebviewController(); viewModel?: LocalResourceViewModel; + // [Start MapRemoteAndLocal] // Map between domain names and local files schemeMap = new Map([ ['https://www.example.com/', 'index.html'], @@ -43,6 +44,7 @@ struct LocalResource { ['index.html', 'text/html'], ['mountain.png', 'image/png'] ]); + // [End MapRemoteAndLocal] aboutToAppear(): void { // Initialize local Resource interceptor @@ -130,13 +132,17 @@ struct LocalResource { */ @Builder WebLoading() { + // [Start MapRemoteAndLocal] Web({ src: this.requestUrl, controller: this.controller }) .onInterceptRequest((event) => { // Update scheme map before intercepting this.viewModel?.updateMappings(this.schemeMap, this.mimeTypeMap); + // [StartExclude MapRemoteAndLocal] // Use request interceptor return this.viewModel?.onInterceptRequest(event) ?? null; + // [EndExclude MapRemoteAndLocal] }) + // [End MapRemoteAndLocal] .javaScriptAccess(true) .fileAccess(true) .domStorageAccess(true) diff --git a/entry/src/main/ets/Interceptors/PageWhitelistInterceptor/model/PageWhitelistModel.ets b/entry/src/main/ets/Interceptors/PageWhitelistInterceptor/model/PageWhitelistModel.ets index 06aef42a71731e9e9bb14ce25cad6b20a87a3115..310a9f7c4d328d4136bc9b84f5e2de65ef9eade2 100644 --- a/entry/src/main/ets/Interceptors/PageWhitelistInterceptor/model/PageWhitelistModel.ets +++ b/entry/src/main/ets/Interceptors/PageWhitelistInterceptor/model/PageWhitelistModel.ets @@ -29,6 +29,7 @@ export class PageWhitelistModel { this.closeWebpageCallback = closeWebpageCallback; } + // [Start ConfigWhitelist] /** * Updates the whitelist URLs */ @@ -38,6 +39,23 @@ export class PageWhitelistModel { .filter(domain => domain.length > 0); } + /** + * Extract the domain name from a URL + */ + private extractDomain(url: string): string { + let normalized = url.trim().toLowerCase(); + // [StartExclude ConfigWhitelist] + if (!normalized) { + return ''; + } + // [EndExclude ConfigWhitelist] + normalized = normalized + .replace(/^(?:[a-z0-9+.-]+:)?\/\//, '') // strip protocol-like prefixes + .split(/[/?#]/)[0]; // drop everything after domain + return normalized.replace(/:+$/, '').replace(/\/+$/, ''); + } + // [End ConfigWhitelist] + /** * Reset session-level allow flags when starting a new load */ @@ -46,6 +64,7 @@ export class PageWhitelistModel { } + // [Start CheckWhitelist] /** * Checks if a URL is in the whitelist */ @@ -56,17 +75,7 @@ export class PageWhitelistModel { } return this.whitelistDomains.includes(requestDomain); } - - private extractDomain(url: string): string { - let normalized = url.trim().toLowerCase(); - if (!normalized) { - return ''; - } - normalized = normalized - .replace(/^(?:[a-z0-9+.-]+:)?\/\//, '') // strip protocol-like prefixes - .split(/[/?#]/)[0]; // drop everything after domain - return normalized.replace(/:+$/, '').replace(/\/+$/, ''); - } + // [End CheckWhitelist] /** * Creates the dialog configuration for blocking non-whitelisted URLs @@ -97,17 +106,20 @@ export class PageWhitelistModel { this.dialogHandler.showDialog(requestUrl, config); } + // [Start NotWhitelist] + // [Start GetRequestUrl] /** * Processes the load intercept event * Returns true if loading should be blocked, false to allow */ processLoadIntercept(event: OnLoadInterceptEvent): boolean { + // [StartExclude NotWhitelist] const requestUrl = event.data.getRequestUrl(); - + // [StartExclude GetRequestUrl] if (this.allowAllForCurrentLoad) { return false; } - + // [EndExclude NotWhitelist] // Check if URL is in whitelist if (this.isUrlInWhitelist(requestUrl)) { this.allowAllForCurrentLoad = true; @@ -117,5 +129,8 @@ export class PageWhitelistModel { // URL not in whitelist, show dialog this.showDialog(requestUrl); return true; // Block loading + // [EndExclude GetRequestUrl] } + // [End GetRequestUrl] + // [Start NotWhitelist] } diff --git a/entry/src/main/ets/Interceptors/PageWhitelistInterceptor/view/PageWhitelistView.ets b/entry/src/main/ets/Interceptors/PageWhitelistInterceptor/view/PageWhitelistView.ets index c9b56e66547c5f7e4e40ce664bfe06d2782bf9f6..dd284af2bd1852482636ab67d2d0563295cdd9fd 100644 --- a/entry/src/main/ets/Interceptors/PageWhitelistInterceptor/view/PageWhitelistView.ets +++ b/entry/src/main/ets/Interceptors/PageWhitelistInterceptor/view/PageWhitelistView.ets @@ -156,13 +156,17 @@ struct PageWhitelist { */ @Builder WebLoading() { + // [Start ConfigWhitelist] Web({ src: this.loadingUrl, controller: this.controller }) .onLoadIntercept((event) => { // Update whitelist URLs before intercepting this.viewModel?.setWhitelistUrls(this.whitelistUrlArr.map(url => url.toString())); + // [StartExclude ConfigWhitelist] // Use load interceptor return this.viewModel?.onLoadIntercept(event) ?? false; + // [EndExclude ConfigWhitelist] }) + // [End ConfigWhitelist] .javaScriptAccess(true) .fileAccess(true) .domStorageAccess(true) diff --git a/entry/src/main/ets/Interceptors/RedirectRequestInterceptor/model/RedirectRequestModel.ets b/entry/src/main/ets/Interceptors/RedirectRequestInterceptor/model/RedirectRequestModel.ets index 4c94d8a0e2dc8036fd9f3ceb3aff1aedd6708e20..21b5b438595f04a6e5d6426eb933e1335580eb59 100644 --- a/entry/src/main/ets/Interceptors/RedirectRequestInterceptor/model/RedirectRequestModel.ets +++ b/entry/src/main/ets/Interceptors/RedirectRequestInterceptor/model/RedirectRequestModel.ets @@ -45,16 +45,7 @@ export class RedirectRequestModel { this.redirectUrl = redirectUrl; } - /** - * Normalizes the URL - */ - private normalizeUrl(url: string): string { - return url - .replace(/^(?:[a-zA-Z]+:)?\/\//, '') - .replace(/\/+$/, '') - .trim(); - } - + // [Start shouldInterceptUrl] /** * Checks if the URL needs to be intercepted and redirected */ @@ -69,31 +60,30 @@ export class RedirectRequestModel { } /** - * Performs the redirect operation + * Normalizes the URL */ - performRedirect(): boolean { - try { - this.controller.loadUrl(this.redirectUrl); - this.allowAllForCurrentLoad = true; - Logger.info(TAG, `Redirected to ${this.redirectUrl}`); - return true; - } catch (error) { - let err = error as BusinessError; - Logger.error(TAG, `Redirect to ${this.redirectUrl} failed, errCode: ${err.code}, errMessage: ${err.message}.`); - return false; - } + private normalizeUrl(url: string): string { + return url + .replace(/^(?:[a-zA-Z]+:)?\/\//, '') + .replace(/\/+$/, '') + .trim(); } + // [End shouldInterceptUrl] + // [Start PerformRedirect] + // [Start GetRequestUrl] /** * Processes the load intercept event * Returns true if loading should be blocked (redirect performed), false to allow */ processLoadIntercept(event: OnLoadInterceptEvent): boolean { + // [StartExclude PerformRedirect] const requestUrl = event.data.getRequestUrl(); - + // [StartExclude GetRequestUrl] if (this.allowAllForCurrentLoad) { return false; } + // [StartExclude PerformRedirect] if (this.shouldInterceptUrl(requestUrl)) { // Perform redirect @@ -102,5 +92,28 @@ export class RedirectRequestModel { } return false; // Allow loading + // [EndExclude GetRequestUrl] + } + // [End GetRequestUrl] + + /** + * Performs the redirect operation + */ + performRedirect(): boolean { + try { + this.controller.loadUrl(this.redirectUrl); + // [StartExclude PerformRedirect] + this.allowAllForCurrentLoad = true; + Logger.info(TAG, `Redirected to ${this.redirectUrl}`); + // [EndExclude PerformRedirect] + return true; + } catch (error) { + // [StartExclude PerformRedirect] + let err = error as BusinessError; + Logger.error(TAG, `Redirect to ${this.redirectUrl} failed, errCode: ${err.code}, errMessage: ${err.message}.`); + // [EndExclude PerformRedirect] + return false; + } } + // [End PerformRedirect] } diff --git a/entry/src/main/ets/common/utils/RcpRequestForwarder.ets b/entry/src/main/ets/common/utils/RcpRequestForwarder.ets index 2221995e963a9a769da5a6f1c6c7638c866fa561..65970f602afdbb1200603bf3bf34ddc544b8cc11 100644 --- a/entry/src/main/ets/common/utils/RcpRequestForwarder.ets +++ b/entry/src/main/ets/common/utils/RcpRequestForwarder.ets @@ -81,6 +81,7 @@ export class RcpRequestForwarder { return result; } + // [Start CreateRcpSession] /** * Creates an RCP session for the next outbound request. */ @@ -93,11 +94,15 @@ export class RcpRequestForwarder { this.session = rcp.createSession(sessionConfig); } catch (error) { + // [StartExclude CreateRcpSession] const err = error as BusinessError; Logger.error(TAG, `Failed to create rcp session, errCode: ${err.code}, errMessage: ${err.message}`); + // [EndExclude CreateRcpSession] } } + // [End CreateRcpSession] + // [Start ForwardRequest] /** * Sends GET or HEAD requests via the RCP session. */ @@ -107,22 +112,31 @@ export class RcpRequestForwarder { resourceHandler: webview.WebResourceHandler, ): void { try { + // [StartExclude ForwardRequest] Logger.info(TAG, `Forward GET: ${targetUrl}`); + // [EndExclude ForwardRequest] this.createSession(headers); this.session?.get(targetUrl).then((response: rcp.Response) => { + // [StartExclude ForwardRequest] Logger.info(TAG, `GET success: ${targetUrl}`); + // [EndExclude ForwardRequest] this.handleResponse(response, resourceHandler); this.session?.close(); }).catch((error: BusinessError) => { + // [StartExclude ForwardRequest] Logger.error(TAG, `Failed to get, errCode: ${error.code}, errMessage: ${error.message}`); this.session?.close(); + // [EndExclude ForwardRequest] }); } catch (error) { + // [StartExclude ForwardRequest] const err = error as BusinessError; Logger.error(TAG, `GET exception, errCode: ${err.code}, errMessage: ${err.message}`); + // [EndExclude ForwardRequest] } } + // [End ForwardRequest] /** * Sends mutation requests and forwards the response back to the web resource. @@ -189,6 +203,8 @@ export class RcpRequestForwarder { } } + // [Start SetResponse] + // [Start HandleResponse] /** * Maps the RCP response to a WebSchemeHandlerResponse. */ @@ -197,10 +213,11 @@ export class RcpRequestForwarder { resourceHandler: webview.WebResourceHandler, ): void { try { + // [StartExclude SetResponse] const webResponse = new webview.WebSchemeHandlerResponse(); webResponse.setStatus(response.statusCode || 200); webResponse.setStatusText('OK'); - + // [StartExclude HandleResponse] let mimeType = 'application/json'; let encoding = 'utf-8'; @@ -222,7 +239,7 @@ export class RcpRequestForwarder { } } } - + // [EndExclude HandleResponse] webResponse.setMimeType(mimeType); webResponse.setEncoding(encoding); webResponse.setNetErrorCode(WebNetErrorList.NET_OK); @@ -233,6 +250,7 @@ export class RcpRequestForwarder { webResponse.setHeaderByName('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS', true); webResponse.setHeaderByName('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Custom-Header', true); + // [StartExclude HandleResponse] if (response.headers) { const headerKeys = Object.keys(response.headers); for (let i = 0; i < headerKeys.length; i++) { @@ -243,13 +261,22 @@ export class RcpRequestForwarder { } } } - + // [EndExclude HandleResponse] resourceHandler.didReceiveResponse(webResponse); resourceHandler.didReceiveResponseBody(response.body); + // [EndExclude SetResponse] + // [StartExclude HandleResponse] resourceHandler.didFinish(); + // [EndExclude HandleResponse] } catch (error) { + // [StartExclude SetResponse] + // [StartExclude HandleResponse] const err = error as BusinessError; Logger.error(TAG, `Handle response failed, errCode: ${err.code}, errMessage: ${err.message}`); + // [EndExclude HandleResponse] + // [EndExclude SetResponse] } } + // [End HandleResponse] + // [End SetResponse] } \ No newline at end of file diff --git a/entry/src/main/ets/component/WhitelistDialog.ets b/entry/src/main/ets/component/WhitelistDialog.ets index 7e2291ac2d5821a48384467319b4ac44077e2679..9e9e1044dfd48b64785635b08dd7c534fd039d24 100644 --- a/entry/src/main/ets/component/WhitelistDialog.ets +++ b/entry/src/main/ets/component/WhitelistDialog.ets @@ -119,14 +119,17 @@ export class WhitelistDialogHandler { } } + // [Start OpenDialog] /** * Open URL in external browser */ private openInBrowser(url: string): void { + // [StartExclude OpenDialog] if (!url.startsWith('http://') && !url.startsWith('https://')) { this.showUrlErrorToast(); return; } + // [EndExclude OpenDialog] const want: Want = { uri: url, @@ -136,24 +139,29 @@ export class WhitelistDialogHandler { 'ohos.ability.params.showDefaultPicker': true } }; - + // [StartExclude OpenDialog] if (!this.uiContext) { Logger.error(TAG, 'UIContext is not set, cannot open browser.'); return; } - + // [EndExclude OpenDialog] const context: common.UIAbilityContext = this.uiContext.getHostContext()! as common.UIAbilityContext; context.startAbility(want) .then(() => { if (this.config?.onOk) { this.config?.onOk(this.targetUrl) } + // [StartExclude OpenDialog] Logger.info(TAG, 'Redirected to open in the browser success!'); + // [EndExclude OpenDialog] }) .catch((err: BusinessError) => { + // [StartExclude OpenDialog] Logger.error(TAG, `Redirect to open in the browser failed, errCode: ${err.code}, errMessage: ${err.message}.`); + // [EndExclude OpenDialog] }); } + // [End OpenDialog] /** * Show URL error toast diff --git a/screenshots/device_en/CommonHeader_en.png b/screenshots/device_en/CommonHeader_en.png index 32f5656afe3c5e779bbed0665c95f0b28b19ca71..0957c1d0bf8a976c909f91d57e625dc554a2526d 100644 Binary files a/screenshots/device_en/CommonHeader_en.png and b/screenshots/device_en/CommonHeader_en.png differ