Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bug] Refused to load remote script in chrome extension script #535

Open
andriy-panchiy opened this issue Sep 17, 2024 · 4 comments
Open

Comments

@andriy-panchiy
Copy link

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.

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

  1. init GoogleAPIMapsProvider in chrome extension

Environment

  • Library version: @vis.gl/react-google-maps@1.1.3
  • Google maps version: weekly
  • Browser and Version: Version 128.0.6613.138 (Official Build) (64-bit)
  • OS: Windows 10

Logs

No response

@andriy-panchiy andriy-panchiy added the bug Something isn't working label Sep 17, 2024
@usefulthink
Copy link
Collaborator

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:

scriptElement.nonce =
(document.querySelector('script[nonce]') as HTMLScriptElement)
?.nonce || '';

Maybe you can provide a link to the site where the problem occurs?

@usefulthink
Copy link
Collaborator

usefulthink commented Sep 17, 2024

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.

@usefulthink usefulthink removed the bug Something isn't working label Sep 17, 2024
@andriy-panchiy
Copy link
Author

if anyone is looking for an answer to a similar question, here's how I solved it:

  1. install package to support Google places types:

npm i @types/google.maps -D

  1. add 'scripting' permission to your manifest.json file.

  2. 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);
})();
  1. In order to maintain the latest version of this file, I also added a run script to package.json:
    image

  2. 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;
};
  1. 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>
    </>
  );
};
  1. Enjoy

@usefulthink
Copy link
Collaborator

usefulthink commented Nov 21, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants