Usar ubicación geográfica

Si quieres obtener información sobre la ubicación geográfica en tu extensión de Chrome, usa la misma API de navigator.geolocation de la plataforma web que usaría normalmente cualquier sitio web. Este artículo existe porque las extensiones de Chrome manejan los permisos para acceder a datos sensibles de manera diferente a los sitios web. La ubicación geográfica es un dato muy sensible, por lo que los navegadores se aseguran de que los usuarios sepan y controlen cuándo y dónde se comparte su ubicación exacta.

Usar la ubicación geográfica en las extensiones MV3

En la Web, los navegadores protegen la seguridad datos de ubicación geográfica mostrando un mensaje en el que se le pide que otorgue a ese origen específico acceso a su ubicación. El mismo modelo de permisos no siempre es apropiado para las extensiones.

Captura de pantalla de la solicitud de permiso que se muestra cuando un sitio web solicita acceso a la API de ubicación geográfica
La solicitud de permiso de ubicación geográfica

Los permisos no son la única diferencia. Como se mencionó anteriormente, navigator.geolocation es una API del DOM, es decir, algo que forma parte de las APIs que conforman los sitios web. Por lo tanto, no se puede acceder a ella dentro de contextos de trabajadores, como el service worker de extensiones, que es la columna vertebral de las extensiones de Manifest V3. Sin embargo, puedes seguir usando geolocation. Solo hay matices sobre cómo y dónde utilizarlo.

Usa la ubicación geográfica en los service workers

No hay ningún objeto navigator dentro de los service workers. Solo está disponible en contextos que tienen acceso al objeto document de una página. Para obtener acceso dentro de un service worker, usa un Offscreen Document, que proporciona acceso a un archivo HTML que puedes empaquetar con tu extensión.

Para comenzar, agrega "offscreen" a la sección "permissions" de tu manifiesto.

manifest.json:

{
  "name": "My extension",
    ...
  "permissions": [
    ...
   "offscreen"
  ],
  ...
}

Después de agregar el permiso "offscreen", agrega un archivo HTML a la extensión que incluya el documento fuera de pantalla. Este caso no utiliza el contenido de la página, por lo que el archivo puede estar casi vacío. Solo necesita ser un pequeño archivo HTML que se cargue en la secuencia de comandos.

offscreen.html:

<!doctype html>
<title>offscreenDocument</title>
<script src="offscreen.js"></script>

Guarda este archivo en la raíz de tu proyecto como offscreen.html.

Como se mencionó, necesitas una secuencia de comandos llamada offscreen.js. También deberás empaquetar esto con tu extensión. Será la fuente de información de ubicación geográfica del service worker. Puedes pasar mensajes entre esta y tu service worker.

offscreen.js:

chrome.runtime.onMessage.addListener(handleMessages);
function handleMessages(message, sender, sendResponse) {
  // Return early if this message isn't meant for the offscreen document.
  if (message.target !== 'offscreen') {
    return;
  }

  if (message.type !== 'get-geolocation') {
    console.warn(`Unexpected message type received: '${message.type}'.`);
    return;
  }

  // You can directly respond to the message from the service worker with the
  // provided `sendResponse()` callback. But in order to be able to send an async
  // response, you need to explicitly return `true` in the onMessage handler
  // As a result, you can't use async/await here. You'd implicitly return a Promise.
  getLocation().then((loc) => sendResponse(loc));

  return true;
}

// getCurrentPosition() returns a prototype-based object, so the properties
// end up being stripped off when sent to the service worker. To get
// around this, create a deep clone.
function clone(obj) {
  const copy = {};
  // Return the value of any non true object (typeof(null) is "object") directly.
  // null will throw an error if you try to for/in it. Just return
  // the value early.
  if (obj === null || !(obj instanceof Object)) {
    return obj;
  } else {
    for (const p in obj) {
      copy[p] = clone(obj[p]);
    }
  }
  return copy;
}

async function getLocation() {
  // Use a raw Promise here so you can pass `resolve` and `reject` into the
  // callbacks for getCurrentPosition().
  return new Promise((resolve, reject) => {
    navigator.geolocation.getCurrentPosition(
      (loc) => resolve(clone(loc)),
      // in case the user doesnt have/is blocking `geolocation`
      (err) => reject(err)
    );
  });
}

Una vez hecho esto, ya puedes acceder al documento fuera de pantalla en el service worker.

chrome.offscreen.createDocument({
  url: 'offscreen.html',
  reasons: [chrome.offscreen.Reason.GEOLOCATION || chrome.offscreen.Reason.DOM_SCRAPING],
  justification: 'geolocation access',
});

Ten en cuenta que cuando accedes a un documento fuera de pantalla, debes incluir un reason. El motivo geolocation no estaba disponible originalmente, por lo que debes especificar un resguardo de DOM_SCRAPING y explicar en la sección justification lo que hace realmente el código. El proceso de revisión de Chrome Web Store utiliza esta información para garantizar que los documentos fuera de pantalla se usen con un fin válido.

Una vez que tengas una referencia al Documento fuera de pantalla, puedes enviarle un mensaje solicitándole información actualizada sobre la ubicación geográfica.

service_worker.js:

const OFFSCREEN_DOCUMENT_PATH = '/offscreen.html';
let creating; // A global promise to avoid concurrency issues

chrome.runtime.onMessage.addListener(handleMessages);

async function getGeolocation() {
  await setupOffscreenDocument(OFFSCREEN_DOCUMENT_PATH);
  const geolocation = await chrome.runtime.sendMessage({
    type: 'get-geolocation',
    target: 'offscreen'
  });
  await closeOffscreenDocument();
  return geolocation;
}

async function hasDocument() {
  // Check all windows controlled by the service worker to see if one
  // of them is the offscreen document with the given path
  const offscreenUrl = chrome.runtime.getURL(OFFSCREEN_DOCUMENT_PATH);
  const matchedClients = await clients.matchAll();

  return matchedClients.some(c => c.url === offscreenUrl)
}

async function setupOffscreenDocument(path) {
  //if we do not have a document, we are already setup and can skip
  if (!(await hasDocument())) {
    // create offscreen document
    if (creating) {
      await creating;
    } else {
      creating = chrome.offscreen.createDocument({
        url: path,
        reasons: [chrome.offscreen.Reason.GEOLOCATION || chrome.offscreen.Reason.DOM_SCRAPING],
        justification: 'add justification for geolocation use here',
      });

      await creating;
      creating = null;
    }
  }
}

async function closeOffscreenDocument() {
  if (!(await hasDocument())) {
    return;
  }
  await chrome.offscreen.closeDocument();
}

Así que cada vez que quieras obtener la ubicación geográfica de tu service worker, solo tienes que llamar al siguiente comando:

const location = await getGeolocation()

Usa la ubicación geográfica en una ventana emergente o un panel lateral

Usar la ubicación geográfica en una ventana emergente o un panel lateral es muy sencillo. Las ventanas emergentes y los paneles laterales son solo documentos web y, por lo tanto, tienen acceso a las APIs normales del DOM. Puedes acceder a navigator.geolocation directamente. La única diferencia con los sitios web estándar es que debes usar el campo "permission" de manifest.json para solicitar el permiso "geolocation". Si no incluyes el permiso, seguirás teniendo acceso a navigator.geolocation. Sin embargo, cualquier intento de usarla provocará un error inmediato, al igual que si el usuario rechazara la solicitud. Puedes verlo en la muestra emergente.

Uso de la ubicación geográfica en una secuencia de comandos de contenido

Al igual que una ventana emergente, una secuencia de comandos de contenido tiene acceso completo a la API del DOM. Sin embargo, los usuarios pasarán por el flujo normal de permisos. Esto significa que agregar "geolocation" a tu "permissions" no te dará acceso automáticamente a las contraseñas la información de ubicación geográfica. Puedes ver esto en el ejemplo de secuencia de comandos de contenido.