Open andriy-panchiy opened 1 month ago
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:
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
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.