(Chrome拡張機能) navigator.clipboard を利用したクリップボードへの貼り付けが httpサイト では失敗する

先日作成した Chrome拡張機能 が httpサイト では上手く動かないことに気付いたので修正しました。

経緯

早速作成した Chrome拡張機能 を運用し始めたのですが、一部のサイトでは次のエラーを出力して上手く動かないことが分かりました。

Unchecked runtime.lastError: The message port closed before a response was received.

原因

いくつかのサイトを試験した結果、 httpサイト ではこのエラーが出力されることが分かりました。

そこで調べてみると……

WebExtension の場合、clipboardRead や clipboardWrite パーミッションを要求することで clipboard.readText() や clipboard.writeText() を使うことができます。HTTPサイトに適用されたコンテンツスクリプトは、Clipboard オブジェクトにアクセスすることはできません。

Clipboard – Web API | MDN

ビンゴ。 httpサイト の Content Script では navigator.clipboard へアクセスできないようです。

対処

navigator.clipboard へアクセスできないとなると、手っ取り早いのは document.execCommand('copy') によるクリップぼ度貼り付けです。

今回は httpサイト という基本レガシーと想定される環境への対処なので、 document.execCommand('copy') で妥協しておきますかね……という感じです。

chain.js

-            chrome.tabs.sendMessage(tab.id, clipText, function(response) {
-                console.log(response.value);
+            chrome.tabs.sendMessage(
+                tab.id,
+                {
+                    text: clipText,
+                    url: tab.url,
+                },
+                function(response) {
+                    console.log(response.value);

chrome.tabs.sendMessage() の第二引数は元はシンプルにクリップボードに貼り付けたいテキストを投げていましたが、今回は http://https:// かの判定をしたかったのでURLのみのシンプルなテキストも送ることにしました。そこで、Stringだった引数をObjectに変更。

smith.js

 chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
     // メッセージとして送信されたクリップボードに貼り付けたいテキストをそのままレスポンスに設定して返却
-    navigator.clipboard.writeText(request);
+    if(request.url.toLowerCase().startsWith('https://')) {
+        navigator.clipboard.writeText(request.text);
+    }
+    else {
+        const inputID = 'chromeExtension-emeraldIsle-clipText';
+        console.log(document.querySelector(`#${inputID}`))
+        if(document.querySelector(`#${inputID}`) === null) {
+            const $input = document.createElement('input');
+            $input.setAttribute('type', 'text');
+            $input.setAttribute('id', inputID);
+            $input.setAttribute('style', 'position: absolute; left: -100vw; top: 0');
+            document.body.appendChild($input);
+        }
+        const $inputDom = document.querySelector(`#${inputID}`);
+        $inputDom.value = request.text;
+        $inputDom.select();
+        document.execCommand('copy');
+    }
     sendResponse({
-        value: request,
+        value: request.text,
     });
+    return true;
 });

content Scripts 側を大改造。元々は navigator.clipboard.writeText(request); のみのシンプルな内容でしたが、次のように処理を変更。

  1. URLに https:// (大文字小文字区別せず) で始まるかどうかを判定
  2. 始まるならば今まで通りの navigator.clipboard.writeText(request.text); で貼り付け
  3. 始まらないならば次の処理を実行
    • もし指定したIDの要素が存在しないならば
      • ID付与、画面外領域に input要素 を追加
    • 再度指定したIDの input要素 を取得、値にクリップボードに貼り付けたいリンク文字列をセット
    • document.execCommand('copy') でクリップボードにコピー

これで httpサイト でもリンク文字列をクリップボードに貼り付けられるようになりました。

参考

Unchecked runtime.lastError: The message port closed before a response was received.

navigator.clipboard へのアクセス

document.execCommand

JavaScript

DOM要素の有無

テキストボックスに値をセット

要素に属性をセット

body の末尾に DOM を挿入

startWith()

toLowerCase()

chrome.tabs.sendMessage, 第二引数 message の型

any だった。

(未使用) html-loader

一瞬 Background Page を作ってそのテキストボックスにいったん貼り付けて、その値を取得する方法を考えました。

今回 Webpack 使用だったので html-loader 案件か、と思いましたが、回避しました。

この記事を書いた人

アルム=バンド

フロントエンド・バックエンド・サーバエンジニア。LAMPやNodeからWP、Gulpを使ってejs,Scss,JSのコーディングまで一通り。たまにRasPiで遊んだり、趣味で開発したり。