monolithic kernel

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

December 24, 2010

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

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

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

    LDRにはキー割り当てを管理するKeybindやアクティブなアイテムを取得するgetactiveitem、棒人間に喋らせる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点目。chromeexoauth.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に変更