monoの開発ブログ

LDRでふぁぼるChrome拡張の内部動作

先日公開したLDRでふぁぼるChrome拡張について少し説明を書いてみようと思います。ソースコードはGitHubに上げてあるので合わせて見ると分かりやすいかもしれません。

この拡張のポイントとなる部分は「Webページの変数へのアクセス」と「Twitter APIの呼び出し」の2点です。

Webページの変数へのアクセス

LDRにはキー割り当てを管理するKeybindやアクティブなアイテムを取得するget_active_item、棒人間に喋らせるmessageなど拡張に便利な機能が多数用意されているため、ぜひ活用したいところです。しかし、拡張機能向けに用意されているContent ScriptはWebページとは隔離された空間で動作するため、変数にアクセスすることができません。

Webページと同じ空間でスクリプトを実行したい場合、Content Script内でscript要素を挿入するか、location.hrefにJavaScriptのコードを代入してあげることで実現できます。

var script = function(shortcut) {
  var onload_ = window.onload;
  window.onload = function() {
    onload_();
    Keybind.add(shortcut, function() {
      // ...
    });
  };
};
location.href = 'javascript:(' + script.toString() + ')(' + JSON.stringify(shortcut) + ')';

Twitter APIの呼び出し

Google Chromeの拡張機能では、裏で動作し続けるBackground Pageを持つことができます。Background Pageでは拡張機能向けのAPIを呼び出すことができるほか、XmlHttpRequestでクロスドメイン通信を行うことができます。Background PageもやはりWebページやContent Scriptとは隔離されており、直接アクセスすることはできませんが、Content Scriptとの間であれば、メッセージをやりとりすることでJSONとして表現可能な範囲のオブジェクトを自由に送受信できます。

// Content Script
var port = chrome.extension.connect({ "name": "FavoReader" });
port.onMessage.addListener(function(m) {
  // receive
  console.log(m);
});
port.postMessage({ type: 'createFavorite', id: n }); // post
// Background Page
chrome.extension.onConnect.addListener(function(port) {
  console.assert(port.name == "FavoReader");
  port.onMessage.addListener(function(m) {
    // receive
    console.log(m);
    // post
    port.postMessage(m);
  });
});

しかし、この方法はContent ScriptとWebページ間の通信には利用できません。Background PageとWebページ間で直接通信することは不可能ですが、Content ScriptとWebページのスクリプトはともにDOMを参照することができるので、ダミーの要素とイベントを使うことで情報をやりとりすることができます。これにより、少々面倒ではありますが、Content Scriptを経由することでWebページとBackground Page間でも情報をやりとりすることができるようになり、Webページで発生したキーイベントによってTwitter APIを呼び出すという目的を達成できます。

// Create element
var element = document.createElement('div');
element.id = 'favoReaderElement';
element.style.display = 'none';
element.addEventListener('onFavoriteEvent', function() {
  var eventData = JSON.parse(element.innerText);
  port.postMessage(eventData);
});
document.body.appendChild(element);
// Web page
var shortcut = settings.options_settings_shortcutkey;
var script = function(shortcut) {
  var element = document.getElementById('favoReaderElement');
  var onload_ = window.onload;
  window.onload = function() {
    onload_();
    var customEvent = document.createEvent('Event');
    customEvent.initEvent('onFavoriteEvent', true, true);
    Keybind.add(shortcut, function() {
      var item = get_active_item(true);
      if(item) {
        var id = null;
        var link = item.link;
        if(link.match(/^http:\/\/twitter\.com\/(?:#!\/)?(\w+)\/statuses\/(\d+)\/?$/)) {
          id = RegExp.$2;
        }
        if(id !== null) {
          // Dispatch event
          element.innerText = Object.toJSON({
            type: 'createFavorite',
            id: id
          });
          element.dispatchEvent(customEvent);
        }
      }
    });
  };
};
location.href = 'javascript:(' + script.toString() + ')(' + JSON.stringify(shortcut) + ')';

また、TwitterのAPIを利用するにはOAuth認証が必須ですが、公式に公開されているサンプルがAccess tokenの取得・保存および署名の付加を勝手にやってくれるのでほとんど考えることは無かったりします。

// Background Page
var oauth = ChromeExOAuth.initBackgroundPage({
  'request_url':     'https://twitter.com/oauth/request_token',
  'authorize_url':   'https://twitter.com/oauth/authorize',
  'access_url':      'https://twitter.com/oauth/access_token',
  'consumer_key':    '',
  'consumer_secret': ''
});
chrome.extension.onConnect.addListener(function(port) {
  console.assert(port.name == "FavoReader");
  port.onMessage.addListener(function(m) {
    if(m.type == 'createFavorite') {
      oauth.authorize(function() {
        var uri = 'https://api.twitter.com/1/favorites/create/' + m.id + '.json';
        var onReceive = function(text, xhr) {
          var status = JSON.parse(text);
        };
        oauth.sendSignedRequest(uri, onReceive, {
          'method': 'POST'
        });
      });
    }
  });
});

マニフェストファイルには以下のように利用するAPIのURLを書いておきます。

{
  ...,
  "permissions": [
    "https://api.twitter.com/1/favorites/create/*.json",
    "https://twitter.com/oauth/request_token",
    "https://twitter.com/oauth/access_token",
    "https://twitter.com/oauth/authorize"
  ],
  ...
}

ただし、注意が必要な点が2つほど。

1点目。アプリケーション登録する時、Application TypeにはBrowserを指定します。Callback URLは認証時に拡張機能内部のURLで上書きされるので適当に指定しておけばOKです。

2点目。chrome_ex_oauth.jsをそのまま使うと余計なパラメータが送信されてRequest tokenの取得に失敗してしまうので、487行付近を修正し、xoauth_displaynameとscopeを送信しないようにしておく必要があります。

var result = OAuthSimple().sign({
  path: this.url_request_token,
  parameters: {
    //xoauth_displayname: this.app_name,
    //scope: this.oauth_scope,
    oauth_callback: url_callback
  },
  signatures: {
    consumer_key: this.consumer_key,
    shared_secret: this.consumer_secret
  }
});

追記

2011/05/23
Twitter APIのURLをhttpsに変更