位置情報を使用する

Chrome 拡張機能で位置情報を取得する場合は、通常のウェブサイトと同じ navigator.geolocation Web Platform API を使用します。この記事は、機密データへのアクセス許可を Chrome 拡張機能がウェブサイトとは異なる方法で処理するためです。位置情報はきわめて機密性の高いデータであるため、ブラウザはユーザーが正確な位置情報を共有するタイミングと場所を完全に把握し、制御できるようにしています。

MV3 拡張機能で位置情報を使用する

ウェブブラウザは、特定のオリジンに対して位置情報へのアクセスを許可するよう求めるプロンプトを表示することで、ユーザーの位置情報を保護します。同じ権限モデルが拡張機能に常にふさわしいとは限りません。

ウェブサイトが位置情報 API へのアクセスを要求したときに表示される権限プロンプトのスクリーンショット
位置情報の利用許可のプロンプト

権限だけではありません。前述のとおり、navigator.geolocationDOM API、つまり、ウェブサイトを構成する API の一部です。そのため、Manifest V3 拡張機能のバックボーンである拡張機能 Service Worker など、ワーカー コンテキスト内ではアクセスできません。ただし、geolocation は引き続き使用できます。ただし、使用する方法と場所が異なります。

Service Worker で位置情報を使用する

Service Worker 内に navigator オブジェクトはありません。ページの document オブジェクトにアクセスできるコンテキスト内でのみ使用できます。Service Worker 内でアクセスするには、Offscreen Document を使用します。これにより、拡張機能にバンドルできる HTML ファイルにアクセスできるようになります。

まず、マニフェストの "permissions" セクションに "offscreen" を追加します。

manifest.json:

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

"offscreen" 権限を追加したら、画面外ドキュメントを含む HTML ファイルを拡張機能に追加します。このケースではページのコンテンツが使用されていないため、ほぼ空のファイルである可能性があります。スクリプトで読み込む小さな HTML ファイルがあれば十分です。

offscreen.html:

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

このファイルをプロジェクトのルートに offscreen.html として保存します。

前述のとおり、offscreen.js というスクリプトが必要です。これを拡張機能にバンドルする必要もあります。これは、Service Worker の位置情報ソースになります。SDK と 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)
    );
  });
}

これで、Service Worker でオフスクリーン ドキュメントにアクセスできるようになりました。

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

画面外ドキュメントにアクセスする場合は、reason を含める必要があります。元々は geolocation の理由が利用できなかったため、代替の DOM_SCRAPING を指定し、コードが実際に何を行っているかを justification セクションで説明します。この情報は、画面外のドキュメントが有効な目的で使用されていることを確認するために、Chrome ウェブストアの審査プロセスで使用されます。

オフスクリーン ドキュメントへの参照を取得したら、そのドキュメントにメッセージを送信して、最新の位置情報を提供するよう依頼できます。

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();
}

これで、Service Worker から位置情報を取得するときはいつでも、以下を呼び出すだけで済みます。

const location = await getGeolocation()

ポップアップやサイドパネルで位置情報を使用する

ポップアップまたはサイドパネル内で位置情報を使用するのはとても簡単です。ポップアップとサイドパネルは単なるウェブ ドキュメントであるため、通常の DOM API にアクセスできます。navigator.geolocation に直接アクセスできます。標準のウェブサイトとの唯一の違いは、manifest.json "permission" フィールドを使用して "geolocation" 権限をリクエストする必要があることです。この権限を含めても、navigator.geolocation には引き続きアクセスできます。ただし、使用しようとすると、ユーザーがリクエストを拒否した場合と同様に、直ちにエラーが発生します。これはポップアップのサンプルで確認できます。

コンテンツ スクリプトで位置情報を使用する

ポップアップと同様に、コンテンツ スクリプトは DOM API に完全にアクセスできますが、ユーザーは通常のユーザー権限フローに進みます。つまり、"permissions""geolocation" を追加しても、ユーザーの位置情報に自動的にアクセスできるわけではありません。詳しくは、コンテンツ スクリプトのサンプルをご覧ください。