位置情報を使用する

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

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

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

<ph type="x-smartling-placeholder">
</ph> ウェブサイトから位置情報 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 による位置情報のソースとなります。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 への完全アクセス権を持ちます。ユーザーは通常のユーザー権限フローを行います。つまり、"geolocation""permissions" 追加しても、ユーザーの位置情報が含まれます。詳しくは、コンテンツ スクリプトのサンプルをご覧ください。