| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import './strings.m.js'; |
| |
| import {assert} from '//resources/js/assert.js'; |
| import {loadTimeData} from '//resources/js/load_time_data.js'; |
| import type {Url} from '//resources/mojo/url/mojom/url.mojom-webui.js'; |
| |
| import type {ImageQuery, LinkOpenMetadata, VisualSearchResult} from './companion.mojom-webui.js'; |
| import {MethodType, PromoAction, PromoType} from './companion.mojom-webui.js'; |
| import type {CompanionProxy} from './companion_proxy.js'; |
| import {CompanionProxyImpl} from './companion_proxy.js'; |
| |
| /** |
| * Method arguments to be passed as part of the JSON message object to be sent |
| * across the postmessage boundary. |
| * Keep this file in sync with |
| * google3/java/com/google/lens/web/interfaces/standalone/companionweb/service/companion_parent_communication_service.ts |
| */ |
| enum ParamType { |
| // Arguments for iframe -> browser communication. |
| // Mandatory arguments. |
| METHOD_TYPE = 'type', |
| |
| // Arguments for MethodType.kOnCqCandidatesAvailable. |
| CQ_TEXT_DIRECTIVES = 'cqTextDirectives', |
| |
| // Optional arguments. |
| // Arguments for MethodType.kOnExpsOptInStatusAvailable. |
| IS_EXPS_OPTED_IN = 'isExpsOptedIn', |
| |
| // Arguments for MethodType.kOnPromoAction. |
| PROMO_ACTION = 'promoAction', |
| PROMO_TYPE = 'promoType', |
| |
| // Arguments for MethodType.kOnPhFeedback. |
| PH_FEEDBACK = 'phFeedback', |
| |
| // Arguments for MethodType.kOnOpenInNewTabButtonURLChanged. |
| URL_FOR_OPEN_IN_NEW_TAB = 'urlForOpenInNewTab', |
| |
| // Arguments for MethodType.kRecordUiSurfaceShown. |
| UI_SURFACE = 'uiSurface', |
| |
| // Arguments for MethodType.kRecordUiSurfaceShown. |
| UI_SURFACE_POSITION = 'uiSurfacePosition', |
| CHILD_ELEMENT_AVAILABLE_COUNT = 'childElementAvailableCount', |
| CHILD_ELEMENT_SHOWN_COUNT = 'childElementShownCount', |
| |
| // Arguments for MethodType.kRecordUiSurfaceClicked. |
| CLICK_POSITION = 'clickPosition', |
| |
| // Arguments for MethodType.kOnCqJamptagClicked. |
| CQ_JUMPTAG_TEXT = 'cqJumptagText', |
| |
| // Arguments for MethodType.kOpenUrlInBrowser |
| URL_TO_OPEN = 'urlToOpen', |
| USE_NEW_TAB = 'useNewTab', |
| |
| // Arguments for MethodType.kNotifyLinkOpen for browser -> iframe |
| // communication. |
| LINK_OPEN_OPENED_URL = 'openedUrl', |
| LINK_OPEN_METADATA = 'openMetadata', |
| |
| // Arguments for browser -> iframe communication. |
| COMPANION_UPDATE_PARAMS = 'companionUpdateParams', |
| |
| // Arguments for sending text find results from browser to iframe. |
| CQ_TEXT_FIND_RESULTS = 'cqTextFindResults', |
| |
| // Arguments for sending Visual Search results from browser to iframe. |
| VISUAL_SEARCH_PARAMS = 'visualSearchParams', |
| |
| // Arguments for sending Visual Search alt text from browser to iframe. |
| VISUAL_SEARCH_IMAGE_ALT_TEXTS = 'visualSearchImageAltTexts', |
| |
| // Arguments for sending companion loading state from iframe to browser. |
| COMPANION_LOADING_STATE = 'companionLoadingState', |
| |
| // Arguments for sending page title from browser to iframe. |
| PAGE_TITLE = 'pageTitle', |
| |
| // Arguments for sending innerHtml from browser to iframe. |
| INNER_HTML = 'innerHtml', |
| } |
| |
| const companionProxy: CompanionProxy = CompanionProxyImpl.getInstance(); |
| |
| // Validation check for incoming enums from the iframe postMessage(). |
| function validatePromoArguments(promoType: any, promoAction: any): boolean { |
| const isValidType = Object.values(PromoType).includes(promoType); |
| const isValidAction = Object.values(PromoAction).includes(promoAction); |
| return isValidType && isValidAction; |
| } |
| |
| function initialize() { |
| // For the initial navigation, we update our iframe src to pass new |
| // URL. |
| companionProxy.callbackRouter.loadCompanionPage.addListener((newUrl: Url) => { |
| const frame = document.body.querySelector('iframe'); |
| assert(frame); |
| frame.src = newUrl.url; |
| }); |
| |
| // For subsequent navigations, we send a post message. |
| companionProxy.callbackRouter.updateCompanionPage.addListener( |
| (companionUpdateProto: string) => { |
| const companionOrigin = |
| new URL(loadTimeData.getString('companion_origin')).origin; |
| const message = { |
| [ParamType.METHOD_TYPE]: MethodType.kUpdateCompanionPage, |
| [ParamType.COMPANION_UPDATE_PARAMS]: companionUpdateProto, |
| }; |
| |
| const frame = document.body.querySelector('iframe'); |
| assert(frame); |
| if (frame.contentWindow) { |
| frame.contentWindow.postMessage(message, companionOrigin); |
| } |
| }); |
| |
| companionProxy.callbackRouter.updatePageContent.addListener( |
| (pageTitle: string, innerHtml: string) => { |
| const companionOrigin = |
| new URL(loadTimeData.getString('companion_origin')).origin; |
| const message = { |
| [ParamType.METHOD_TYPE]: MethodType.kUpdatePageContent, |
| [ParamType.PAGE_TITLE]: pageTitle, |
| [ParamType.INNER_HTML]: innerHtml, |
| }; |
| |
| const frame = document.body.querySelector('iframe'); |
| assert(frame); |
| if (frame.contentWindow) { |
| frame.contentWindow.postMessage(message, companionOrigin); |
| } |
| }); |
| |
| // On image queries, we need to send a POST to the iframe using a form in the |
| // WebUI. |
| companionProxy.callbackRouter.onImageQuery.addListener( |
| (imageQuery: ImageQuery) => { |
| const queryForm = document.body.querySelector('form'); |
| const imageDataInput = |
| document.getElementById('image-data') as HTMLInputElement; |
| const imageUrlInput = |
| document.getElementById('image-src-url') as HTMLInputElement; |
| const widthInput = |
| document.getElementById('image-width') as HTMLInputElement; |
| const heightInput = |
| document.getElementById('image-height') as HTMLInputElement; |
| const downscaledDimensionsInput = |
| document.getElementById('image-downscaled-dimensions') as |
| HTMLInputElement; |
| assert(queryForm); |
| assert(imageDataInput); |
| assert(imageUrlInput); |
| assert(widthInput); |
| assert(heightInput); |
| assert(downscaledDimensionsInput); |
| queryForm.setAttribute('action', imageQuery.uploadUrl.url); |
| // The original Uint8Array that gets passed does not have an array |
| // buffer due to how it is initialized. Thus, we have to create a |
| // Uint8Array with the same data as |imageBytes| in order to properly |
| // create a blob from it. |
| const imageBytesWithBuffer = new Uint8Array(imageQuery.imageBytes); |
| const blob = |
| new Blob([imageBytesWithBuffer], {type: imageQuery.contentType}); |
| const file = |
| new File([blob], 'filename.jpg', {type: imageQuery.contentType}); |
| |
| // Create a DataTransfer to create a file list for the images we want to |
| // query. |
| const container = new DataTransfer(); |
| container.items.add(file); |
| |
| // Assign all values on the form and submit to initiate request. |
| imageDataInput.files = container.files; |
| imageUrlInput.value = imageQuery.imageUrl.url; |
| widthInput.value = String(imageQuery.width); |
| heightInput.value = String(imageQuery.height); |
| downscaledDimensionsInput.value = |
| `${imageQuery.downscaledWidth},${imageQuery.downscaledHeight}`; |
| queryForm.submit(); |
| queryForm.reset(); |
| }); |
| |
| companionProxy.callbackRouter.onCqFindTextResultsAvailable.addListener( |
| (textDirectives: string[], results: boolean[]) => { |
| const companionOrigin = |
| new URL(loadTimeData.getString('companion_origin')).origin; |
| const message = { |
| [ParamType.METHOD_TYPE]: MethodType.kOnCqFindTextResultsAvailable, |
| [ParamType.CQ_TEXT_DIRECTIVES]: textDirectives, |
| [ParamType.CQ_TEXT_FIND_RESULTS]: results, |
| }; |
| |
| const frame = document.body.querySelector('iframe'); |
| assert(frame); |
| if (frame.contentWindow) { |
| frame.contentWindow.postMessage(message, companionOrigin); |
| } |
| }); |
| |
| // POST dataUris from the Visual Search classification results to the iframe |
| companionProxy.callbackRouter.onDeviceVisualClassificationResult.addListener( |
| (results: VisualSearchResult[]) => { |
| const dataUris = results.map(result => result.dataUri); |
| const altTexts = results.map(result => result.altText); |
| const message = { |
| [ParamType.METHOD_TYPE]: |
| MethodType.kOnDeviceVisualClassificationResult, |
| [ParamType.VISUAL_SEARCH_PARAMS]: dataUris, |
| [ParamType.VISUAL_SEARCH_IMAGE_ALT_TEXTS]: altTexts, |
| }; |
| |
| const companionOrigin = |
| new URL(loadTimeData.getString('companion_origin')).origin; |
| const frame = document.body.querySelector('iframe'); |
| assert(frame); |
| if (frame.contentWindow) { |
| frame.contentWindow.postMessage(message, companionOrigin); |
| } |
| }); |
| |
| companionProxy.callbackRouter.onNavigationError.addListener(() => { |
| const networkErrorOverlay = document.getElementById('network-error-page'); |
| const frame = document.body.querySelector('iframe'); |
| assert(frame); |
| assert(networkErrorOverlay); |
| |
| // Hide the frame and show the network error overlay. |
| networkErrorOverlay.style.display = 'block'; |
| frame.style.display = 'none'; |
| }); |
| |
| companionProxy.callbackRouter.notifyLinkOpen.addListener( |
| (openedUrl: Url, metadata: LinkOpenMetadata) => { |
| const companionOrigin = |
| new URL(loadTimeData.getString('companion_origin')).origin; |
| const message = { |
| [ParamType.METHOD_TYPE]: MethodType.kNotifyLinkOpen, |
| [ParamType.LINK_OPEN_OPENED_URL]: openedUrl.url, |
| [ParamType.LINK_OPEN_METADATA]: metadata, |
| }; |
| |
| const frame = document.body.querySelector('iframe'); |
| assert(frame); |
| if (frame.contentWindow) { |
| frame.contentWindow.postMessage(message, companionOrigin); |
| } |
| }); |
| |
| companionProxy.handler.showUI(); |
| } |
| |
| // Handler for postMessage() calls from the embedded iframe. |
| function onCompanionMessageEvent(event: MessageEvent) { |
| // Because the |companion_origin| string has a trailing slash that can cause |
| // failures when doing a string comparison, convert the string to a URL and |
| // compare the origin to prevent failures when origins are the same but |
| // strings differ. |
| const validOrigin = |
| new URL(loadTimeData.getString('companion_origin')).origin; |
| if (validOrigin !== event.origin) { |
| return; |
| } |
| |
| const data = event.data; |
| const methodType = data[ParamType.METHOD_TYPE]; |
| if (methodType === MethodType.kOnRegionSearchClicked) { |
| companionProxy.handler.onRegionSearchClicked(); |
| } else if (methodType === MethodType.kOnPromoAction) { |
| const promoType = data[ParamType.PROMO_TYPE]; |
| const promoAction = data[ParamType.PROMO_ACTION]; |
| if (validatePromoArguments(promoType, promoAction)) { |
| companionProxy.handler.onPromoAction(promoType, promoAction); |
| } |
| } else if (methodType === MethodType.kOnExpsOptInStatusAvailable) { |
| companionProxy.handler.onExpsOptInStatusAvailable( |
| data[ParamType.IS_EXPS_OPTED_IN]); |
| } else if (methodType === MethodType.kOnOpenInNewTabButtonURLChanged) { |
| const openInNewTabUrl: Url = {url: data[ParamType.URL_FOR_OPEN_IN_NEW_TAB]}; |
| companionProxy.handler.onOpenInNewTabButtonURLChanged(openInNewTabUrl); |
| } else if (methodType === MethodType.kRecordUiSurfaceShown) { |
| const uiSurfacePosition = data[ParamType.UI_SURFACE_POSITION] || -1; |
| const childElementAvailableCount = |
| data[ParamType.CHILD_ELEMENT_AVAILABLE_COUNT] || -1; |
| const childElementShownCount = |
| data[ParamType.CHILD_ELEMENT_SHOWN_COUNT] || -1; |
| companionProxy.handler.recordUiSurfaceShown( |
| data[ParamType.UI_SURFACE], uiSurfacePosition, |
| childElementAvailableCount, childElementShownCount); |
| } else if (methodType === MethodType.kRecordUiSurfaceClicked) { |
| const clickPosition = data[ParamType.CLICK_POSITION] || -1; |
| companionProxy.handler.recordUiSurfaceClicked( |
| data[ParamType.UI_SURFACE], clickPosition); |
| } else if (methodType === MethodType.kOnCqCandidatesAvailable) { |
| companionProxy.handler.onCqCandidatesAvailable( |
| data[ParamType.CQ_TEXT_DIRECTIVES]); |
| } else if (methodType === MethodType.kOnPhFeedback) { |
| companionProxy.handler.onPhFeedback(data[ParamType.PH_FEEDBACK]); |
| } else if (methodType === MethodType.kOnCqJumptagClicked) { |
| companionProxy.handler.onCqJumptagClicked(data[ParamType.CQ_JUMPTAG_TEXT]); |
| } else if (methodType === MethodType.kOpenUrlInBrowser) { |
| const urlToOpen: Url = {url: data[ParamType.URL_TO_OPEN] || ''}; |
| companionProxy.handler.openUrlInBrowser( |
| urlToOpen, data[ParamType.USE_NEW_TAB]); |
| } else if (methodType === MethodType.kCompanionLoadingState) { |
| companionProxy.handler.onLoadingState( |
| data[ParamType.COMPANION_LOADING_STATE]); |
| } else if (methodType === MethodType.kRefreshCompanionPage) { |
| companionProxy.handler.refreshCompanionPage(); |
| } else if (methodType === MethodType.kServerSideUrlFilterEvent) { |
| companionProxy.handler.onServerSideUrlFilterEvent(); |
| } |
| } |
| |
| window.addEventListener('message', onCompanionMessageEvent, false); |
| document.addEventListener('DOMContentLoaded', initialize); |