[Bug] Refused to load remote script in chrome extension script
Description
Refused to load the script 'https://maps.googleapis.com/maps/api/js?key=secret&language=en-US&solution_channel=GMP_visgl_rgmlibrary_v1_default&loading=async&callback=googleMapsCallback' because it violates the following Content Security Policy directive: "script-src 'self'". Note that 'script-src-elem' was not explicitly set, so 'script-src' is used as a fallback.
<ApiProvider> failed to load the Google Maps JavaScript API Error: The Google Maps JavaScript API could not load. at scriptElement.onerror (index.umd.js:182:1)
Steps to Reproduce
- init GoogleAPIMapsProvider in chrome extension
Environment
- Library version: @vis.gl/[email protected]
- Google maps version: weekly
- Browser and Version: Version 128.0.6613.138 (Official Build) (64-bit)
- OS: Windows 10
Logs
No response
I'm not an expert on CSP issues, but I think this could help: https://developers.google.com/maps/documentation/javascript/content-security-policy
Note that we will reuse the nonce-value of the first script-tag that has one:
https://github.com/visgl/react-google-maps/blob/40b47d4894cfbea1edfc627513f7713db499eea5/src/libraries/google-maps-api-loader.ts#L149-L151
Maybe you can provide a link to the site where the problem occurs?
Just noticed the part about this being in a chrome extension.
I think I read somewhere that external scripts will no longer be supported in chrome extensions as of manifest version 3. Sadly, this also includes the google maps API.
EDIT Bad news: https://developer.chrome.com/docs/extensions/develop/migrate/remote-hosted-code
You might have to reach out to the Chrome Extensions DevRel folks to see if they can help you with what you want to achieve.
if anyone is looking for an answer to a similar question, here's how I solved it:
- install package to support Google places types:
npm i @types/google.maps -D
-
add 'scripting' permission to your manifest.json file.
-
In order not to download the .js file used in the library - we can download it even before the build version of the extension is created, this will allow you to bypass the problem of chrome policy regarding remote code For these purposes, this code was enough for me:
pre-build.ts:
import * as fs from 'fs'; import config from './config/config.json'; const apiKey = config.GOOGLE_MAPS_API_KEY; (async () => { const libraries = ['places'].join(','); const response = await fetch(`https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=${libraries}`); const data = await response.text(); fs.writeFileSync('./src/inject/googlePlaces.js', data); })();
-
In order to maintain the latest version of this file, I also added a run script to package.json:
-
Since my extension is written on the basis of React, I created a React hook to interact with the GooglePlacesAPI:
useGooglePlaces.ts:
import { useEffect } from 'react'; import { useDebouncedCallback } from 'use-debounce'; export type GooglePlacesAutocompleteHandle = { getSessionToken: () => google.maps.places.AutocompleteSessionToken | undefined; refreshSessionToken: () => void; }; export interface LatLng { lat: number; lng: number; } export interface AutocompletionRequest { bounds?: [LatLng, LatLng]; componentRestrictions?: { country: string | string[] }; location?: LatLng; offset?: number; radius?: number; types?: string[]; } export default interface GooglePlacesAutocompleteProps { autocompletionRequest?: AutocompletionRequest; debounce?: number; minLengthAutocomplete?: number; onLoadFailed?: (error: Error) => void; withSessionToken?: boolean; } export const useGooglePlacesAutocomplete = ({ autocompletionRequest = {}, debounce = 300, minLengthAutocomplete = 0, onLoadFailed = console.error, withSessionToken = false, }: GooglePlacesAutocompleteProps): ((value: string, cb: (options: google.maps.places.AutocompletePrediction[]) => void) => void) => { const [fetchSuggestions] = useDebouncedCallback(async (value: string, cb: (options: google.maps.places.AutocompletePrediction[]) => void) => { const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); if (!tab?.id) return cb([]); const [res] = await chrome.scripting.executeScript({ target: { tabId: tab.id }, world: 'MAIN', func: async (value: string, minLengthAutocomplete: number, withSessionToken: boolean, autocompletionRequest: AutocompletionRequest): Promise<google.maps.places.AutocompletePrediction[]> => { if (!window.google) throw new Error('[react-google-places-autocomplete]: Google script not loaded'); if (!window.google.maps) throw new Error('[react-google-places-autocomplete]: Google maps script not loaded'); if (!window.google.maps.places) throw new Error('[react-google-places-autocomplete]: Google maps places script not loaded'); const PlacesService = new google.maps.places.AutocompleteService(); const SessionToken = new google.maps.places.AutocompleteSessionToken(); console.log('value', value); if (value.length < minLengthAutocomplete) return []; const autocompletionRequestBuilder = ( autocompletionRequest: AutocompletionRequest, input: string, sessionToken?: google.maps.places.AutocompleteSessionToken, ): google.maps.places.AutocompletionRequest => { const { bounds, location, componentRestrictions, offset, radius, types } = autocompletionRequest; const res: google.maps.places.AutocompletionRequest = { input, componentRestrictions, offset, radius, types, ...(sessionToken ? { sessionToken: SessionToken } : {}), ...(bounds ? { bounds: new google.maps.LatLngBounds(...bounds) } : {}), ...(location ? { location: new google.maps.LatLng(location) } : {}), }; return res; }; const waitPromise = <T>(promise: Promise<T>, timeout: number): Promise<T | Error> => { return Promise.race([promise, new Promise<Error>((_, reject) => setTimeout(() => reject(new Error('Timeout')), timeout))]); }; const data = PlacesService.getPlacePredictions(autocompletionRequestBuilder(autocompletionRequest, value, withSessionToken && SessionToken)); const res = await waitPromise(data, 5000); if (!(res instanceof Error)) return res.predictions; return []; }, args: [value, minLengthAutocomplete, withSessionToken, autocompletionRequest], }); if (res) { return cb(res.result); } }, debounce); const init = async () => { try { const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); if (!tab?.id) return; if (!window.google || !window.google.maps || !window.google.maps.places) { await chrome.scripting.executeScript({ target: { tabId: tab.id }, world: 'MAIN', files: ['inject/googlePlaces.js'], }); } } catch (error) { onLoadFailed(new Error(String(error))); } }; useEffect(() => { init(); }, []); return fetchSuggestions; };
- Usage:
page.tsx:
import { useGooglePlacesAutocomplete } from '@/library/hooks/useGooglePlaces'; export const Example = (props) => { const [autocompleteData, setAutocompleteData] = useState<google.maps.places.AutocompletePrediction[]>([]); const autocomplete = useGooglePlacesAutocomplete({ debounce: 300, minLengthAutocomplete: 3 }); return ( <> <input type='text' onChange={(e) => autocomplete(e.target.value, setAutocompleteData)} /> <ul> {autocompleteData.map((item, index) => <li key={index}>{item.description}</li>)} </ul> </> ); };
- Enjoy
- If anyone has any questions, you can email me at: [email protected]
I think it is important to note here that downloading the library-files and re-publishing them is very likely a violation of the Google Maps Platform Terms of Service (I am not a lawyer) and definitely something I would advise against, especially for commercial purposes.
If you only need the Places and Autocomplete functions, it might be better to directly use the Places API via fetch and without the Maps JavaScript API and this library.